@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,34 +1,80 @@
1
+ import { isDirectModelId, ModelUnavailableError } from "@opengeni/contracts";
2
+ import { requireExplicitPermissionDelegation } from "../access";
3
+ import { acceptSessionFileAttachments } from "@opengeni/db";
1
4
  import { knowledgeContextForAccess } from "./knowledge";
5
+ import {
6
+ getSessionEvent,
7
+ getSessionRetryReceiptInTransaction,
8
+ setSessionModelInTransaction,
9
+ withWorkspaceSessionActivityRls,
10
+ retryFailedSessionInTransaction,
11
+ SessionRetryConflictError,
12
+ } from "@opengeni/db";
13
+ import type {
14
+ SessionRetryRequest,
15
+ SessionRetryResponse,
16
+ SessionTurnSurface,
17
+ } from "@opengeni/contracts";
18
+ import { resolveTurnSurface } from "../turn-surface";
19
+ import {
20
+ recordSessionCreated,
21
+ recordUserMessageAccepted,
22
+ type ProductUsageMetricsSink,
23
+ } from "../product-usage-metrics";
2
24
  import { saveAgentLearningSettings } from "@opengeni/db";
3
25
  import { withSessionRlsActorContext } from "@opengeni/db";
4
26
  import { fileOwnerContextForAccess, fileOwnerContextForAgent } from "./file-owner";
27
+ import { assertSessionIsNotImported } from "@opengeni/db";
5
28
  import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
6
- import { sessionCreationMetadata } from "../site-session-origin";
7
29
  import {
8
- hostMcpBindingMatchesSelection,
9
- HostMcpCreateSelections,
10
- type HostMcpCreateSelection,
11
- } from "@opengeni/contracts/host-mcp-bindings";
30
+ currentSiteSessionOrigin,
31
+ sessionCreationMetadata,
32
+ SITE_ORIGIN_METADATA_KEY,
33
+ } from "../site-session-origin";
34
+ import { assertSiteAgentMayChooseCompute } from "../site-agent-clamp";
35
+ import { currentHostSessionUsageLimit } from "../host-session-usage-limit";
36
+ import { measureSessionStartPhase, type SessionStartObservability } from "./session-start-timing";
12
37
  import {
13
- captureDirectHostMcpAuthority,
14
- HostMcpDelegationAuthorityError,
15
- HostMcpBindingConflictError,
16
- getHostMcpBinding,
17
- getHostMcpDelegation,
18
- } from "@opengeni/db";
38
+ requireDeveloperSetupDelegatedPermissions,
39
+ withDeveloperSetupCredentialRestriction,
40
+ } from "../developer-setup-credential-ceiling";
41
+
19
42
  import {
43
+ CLAUDE_CONNECTION_KINDS,
44
+ claudeProviderId,
20
45
  canonicalizeConfiguredModelId,
21
46
  configuredAllowedModels,
47
+ withCodexCatalogProvider,
22
48
  ORGANIZATION_GATEWAY_MODEL_ID_PREFIX,
23
49
  ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX,
50
+ ORGANIZATION_OPPER_MODEL_ID_PREFIX,
51
+ WORKSPACE_OPPER_MODEL_ID_PREFIX,
52
+ allowedFirstPartyMcpToolsForSession,
53
+ resolveSessionFirstPartyMcpTools,
54
+ deploymentUnavailableFirstPartyMcpTools,
24
55
  resolveFirstPartyMcpToolPolicy,
56
+ policyChargesCreditsForModel,
25
57
  policyProviderIdForModel,
26
58
  resolveTurnExecutionPolicyV1,
27
59
  WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
28
60
  WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
29
61
  XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
62
+ codeSearchDeploymentPolicy,
63
+ agentConfigDeploymentPolicy,
30
64
  type Settings,
31
65
  } from "@opengeni/config";
66
+ import type { CodeSearchDeploymentPolicy } from "@opengeni/contracts/code-search";
67
+ import {
68
+ agentConfigAddedFirstPartyMcpTools,
69
+ intersectVaultPolicies,
70
+ resolveAgentConfigUpdate,
71
+ VAULT_POLICY_ALL,
72
+ type AgentConfigCreator,
73
+ type ResolvedAgentCapabilities,
74
+ type ResolvedAgentConfig,
75
+ type UpdateSessionAgentRequest,
76
+ type VaultPolicy,
77
+ } from "@opengeni/contracts";
32
78
  import {
33
79
  AUTOMATIC_SESSION_TITLE_FALLBACK,
34
80
  CreateSessionRequest,
@@ -40,6 +86,7 @@ import {
40
86
  OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
41
87
  SessionSkills,
42
88
  resolveBundledSkillSelection,
89
+ bundledSkillSelectionForAgentConfig,
43
90
  SessionSpawnDenial,
44
91
  ServiceTurnInitiator,
45
92
  ServiceTurnInitiatorContext,
@@ -55,7 +102,9 @@ import {
55
102
  type GoalSpec,
56
103
  type FirstPartyMcpToolName,
57
104
  type McpPersonalConnectionDelegation,
58
- type McpConnectionAuthoritySelection,
105
+ type McpConnectionAccountBinding,
106
+ type ConnectionAccountIntent,
107
+ type McpConnectionAccountSelection,
59
108
  type Permission,
60
109
  type PersonalResourceAttachmentIntent,
61
110
  type ReasoningEffort,
@@ -68,14 +117,18 @@ import {
68
117
  type SessionSkill,
69
118
  type SessionEvent,
70
119
  SessionMcpApprovalPolicy,
120
+ SetSessionModelRequest,
71
121
  type SessionMcpCredentialUpdateInput,
72
122
  type SessionMcpServerInput,
73
123
  type SessionMcpServerMetadata,
124
+ type SessionMcpApprovalPolicyTarget,
74
125
  type SubmittedTimelineAnnotation,
75
126
  type TimelineAnnotation,
76
127
  type UpdateSessionMcpApprovalPolicyResponse,
77
128
  type UpdateSessionToolPolicyRequest,
129
+ type UpdateSessionSkillsRequest,
78
130
  type SessionAuthorizationPort,
131
+ type SessionAuthorizationSurface,
79
132
  type SessionToolPolicy,
80
133
  type SessionTurn,
81
134
  type SessionPromptRouting,
@@ -86,36 +139,44 @@ import {
86
139
  type TurnExecutionPolicyV1,
87
140
  type VariableSet,
88
141
  type XaiProviderAccountAuthoritySnapshotV1,
142
+ type ClaudeProviderAccountAuthoritySnapshotV1,
89
143
  } from "@opengeni/contracts";
90
144
  import {
91
145
  assertExactNewSessionDraftInTransaction,
92
146
  createSession,
93
147
  createSessionWithIdempotencyKeyResult,
94
148
  canonicalSessionCommandHash,
149
+ frozenInitiatorForCommandActor,
150
+ getComposerDraftInTransaction,
95
151
  encryptVariableSetValue,
96
152
  getAnySessionInGroup,
97
153
  getEnrollment,
98
154
  getChannel,
99
155
  getRig,
100
156
  getWorkspaceDefaultRigId,
101
- listDistinctVariableSetSelectionsInGroup,
102
157
  listDistinctRigVersionIdsInGroup,
103
158
  listInstalledPortableSkills,
159
+ listEnabledMcpCapabilityServers,
160
+ resolveMcpApprovalRecommendation,
104
161
  getSandbox,
105
162
  getSession,
106
163
  getInitializedSessionCreateReplay,
164
+ getSessionGoal,
107
165
  getSessionAuthorityProjection,
108
166
  SessionIdConflictError,
109
167
  NewSessionDraftConflictError,
110
168
  getWorkspaceControlEvent,
111
169
  getSessionLineage,
112
170
  getSessionTurn,
171
+ getSessionTurnInitiatingHumanSubjectId,
113
172
  getSessionTurnForAttempt,
114
173
  getSessionTurnPersonalConnectionDelegations,
115
174
  getSessionTurnXaiProviderAccountAuthoritySnapshot,
175
+ getSessionTurnClaudeProviderAccountAuthoritySnapshot,
116
176
  getWorkspaceModelPolicy,
117
177
  requireWorkspace,
118
178
  initializeSessionStartAtomically,
179
+ discardUninitializedSessionShell,
119
180
  listSessionTurns,
120
181
  listSessionMcpServersForChildInheritance,
121
182
  requireSession,
@@ -125,9 +186,12 @@ import {
125
186
  submitHumanPromptInTransaction,
126
187
  appendSessionEventsWithLockedSessionUpdate,
127
188
  updateSessionTitleWithEvent,
189
+ setSessionKeepLive,
190
+ type SetSessionKeepLiveResult,
128
191
  withWorkspaceSubjectSessionActivityRls,
129
192
  type CreateSessionMcpServerInput,
130
193
  type Database,
194
+ type FrozenTurnInitiator,
131
195
  type UpdateSessionMcpServerCredentialsInput,
132
196
  QueueCommandConflictError,
133
197
  AgentCommandAuthorityError,
@@ -137,7 +201,6 @@ import {
137
201
  SessionToolPolicyVersionConflictError,
138
202
  SessionCreateIdempotencyConflictError,
139
203
  PersonalResourceAttachmentAcceptanceError,
140
- sessionTenancyProductActivated,
141
204
  workspaceControlRequestLockTimeoutMs,
142
205
  WorkspaceControlBusyError,
143
206
  type SessionCommandActor,
@@ -153,8 +216,16 @@ import { hasPermission, requirePermission, type AccessGrantAuthorization } from
153
216
  import { externalCreationMetadata } from "./external-creation-attribution";
154
217
  import { prepareExternalLinkTurnAdmission } from "../application/external-link-work-admission";
155
218
  import { externalContinuationCommitAuthorizer } from "../application/external-continuation";
156
- import { prepareHostMcpOwnerAuthorization } from "../application/host-mcp-owner";
219
+
157
220
  import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
221
+ import {
222
+ applyInitialSessionUsageLimit,
223
+ requireSessionUsageLimitAllows,
224
+ SessionUsageLimitHttpError,
225
+ sessionUsageLimitsProducerEnabled,
226
+ sessionUsageLimitWriteActorForAccess,
227
+ } from "../session-usage-limits";
228
+ import { recordSessionUsageLimitEvent } from "../session-usage-limit-metrics";
158
229
  import type {
159
230
  AcceptSessionUserMessageDependencies,
160
231
  ApiRouteDeps,
@@ -166,7 +237,7 @@ import {
166
237
  requireSessionAuthorization,
167
238
  SessionAuthorizationDeniedError,
168
239
  } from "../session-authorization";
169
- import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
240
+ import { assertNativeMcpConnectionRef } from "./native-mcp-connection-admission";
170
241
  import {
171
242
  preflightCreateTimeSandboxTarget,
172
243
  swapActiveSandbox,
@@ -179,11 +250,26 @@ import {
179
250
  resolveWorkspaceCatalogSettings,
180
251
  workspaceCustomModelReference,
181
252
  } from "../model-catalog";
253
+ import {
254
+ resolveDefaultSessionModel,
255
+ resolveCallerWorkspaceModelSelections,
256
+ admissibleWorkspaceModel,
257
+ } from "../default-session-model";
182
258
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
259
+ import {
260
+ applySessionAgentConfigWriteThrough,
261
+ legacySessionAgentCapabilities,
262
+ resolveSessionAgentConfigForCreate,
263
+ withAgentConfigHttpErrors,
264
+ } from "./agent-config-resolution";
265
+ import {
266
+ resolveSessionToolPolicy,
267
+ workspaceSessionToolPolicyDefaultServerIdsFor,
268
+ } from "./session-tool-policy";
183
269
  import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
184
- import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
270
+ import { requireEnvironmentEncryption, variableSetsRemovedError } from "./encryption-key";
185
271
  import {
186
- freezePersonalConnectionDelegations,
272
+ freezeConnectionAccounts,
187
273
  personalConnectionDelegationSourceForGrant,
188
274
  } from "./personal-connection-delegations";
189
275
  import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
@@ -209,12 +295,40 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
209
295
  // the generic lossy projection cannot silently rewrite this audit fact.
210
296
  const maxToolPolicyAuditRefs = 40;
211
297
 
298
+ function withoutExcludedMcpServers(
299
+ tools: ToolRef[],
300
+ excludedIds: readonly string[] = [],
301
+ ): ToolRef[] {
302
+ const excluded = new Set(excludedIds);
303
+ return tools.filter((tool) => tool.id === "opengeni" || !excluded.has(tool.id));
304
+ }
305
+
306
+ function defaultPolicyExclusions(
307
+ ids: readonly string[] = [],
308
+ ): Pick<SessionToolPolicy, "excludedMcpServerIds"> {
309
+ const sorted = [...new Set(ids)].filter((id) => id !== "opengeni").sort();
310
+ if (sorted.length > 64 || sorted.some((id) => id.length > 200 || !/^[A-Za-z0-9_-]+$/.test(id))) {
311
+ throw new HTTPException(422, {
312
+ message: "connector exclusions must contain at most 64 valid MCP server IDs",
313
+ });
314
+ }
315
+ return sorted.length ? { excludedMcpServerIds: sorted } : {};
316
+ }
317
+
212
318
  function isCatalogOverlayModel(modelId: string | null | undefined): boolean {
213
319
  return (
320
+ (modelId != null && isDirectModelId(modelId)) ||
214
321
  modelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
215
322
  modelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) === true ||
216
323
  modelId?.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX) === true ||
217
- modelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX) === true
324
+ modelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX) === true ||
325
+ modelId?.startsWith(WORKSPACE_OPPER_MODEL_ID_PREFIX) === true ||
326
+ modelId?.startsWith(ORGANIZATION_OPPER_MODEL_ID_PREFIX) === true ||
327
+ CLAUDE_CONNECTION_KINDS.some((kind) =>
328
+ ["workspace", "organization"].some((scope) =>
329
+ modelId?.startsWith(claudeProviderId(kind, scope as "workspace" | "organization") + "/"),
330
+ ),
331
+ )
218
332
  );
219
333
  }
220
334
  // RFC 9110 field-name token characters.
@@ -241,11 +355,6 @@ async function requireAtomicPersonalResourceAttachment(
241
355
  cause: error,
242
356
  });
243
357
  }
244
- if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
245
- throw new HTTPException(409, {
246
- message: "Session tenancy is not activated for this organization.",
247
- });
248
- }
249
358
  if (existingSession && intent.expectedAuthorityEpoch === undefined) {
250
359
  throw new HTTPException(422, {
251
360
  message: "Personal-resource attachment requires expectedAuthorityEpoch.",
@@ -307,6 +416,13 @@ export function resolveFirstPartyMcpToolsForCreate(
307
416
  },
308
417
  ): FirstPartyMcpToolName[] {
309
418
  if (requested !== undefined) {
419
+ // Check the caller's own names, before legacy aliases expand them.
420
+ requireDeploymentFirstPartyMcpTools(
421
+ requested,
422
+ policy.allowed,
423
+ [],
424
+ "Omit firstPartyMcpTools to inherit the effective selection.",
425
+ );
310
426
  requested = currentAgentLearningToolSelection(requested);
311
427
  if (parentStored !== undefined) {
312
428
  const parentCeiling = effectiveFirstPartyMcpToolCeiling(parentStored, policy);
@@ -324,6 +440,29 @@ export function resolveFirstPartyMcpToolsForCreate(
324
440
  return currentAgentLearningToolSelection(inherited).filter((tool) => allowed.has(tool));
325
441
  }
326
442
 
443
+ /**
444
+ * An explicit first-party tool selection is a promise, not a best-effort
445
+ * default: a tool the deployment disables fails the request, naming every such
446
+ * tool and the allowed set, instead of silently shrinking the selection.
447
+ * `retained` names tools the caller may echo from its stored selection; those
448
+ * stay deployment-filtered like any default, so a later policy change never
449
+ * blocks an unrelated edit.
450
+ */
451
+ export function requireDeploymentFirstPartyMcpTools(
452
+ requested: readonly FirstPartyMcpToolName[],
453
+ allowed: readonly FirstPartyMcpToolName[],
454
+ retained: Iterable<FirstPartyMcpToolName> = [],
455
+ hint = "",
456
+ ): void {
457
+ const permitted = new Set([...allowed, ...retained]);
458
+ const disallowed = [...new Set(requested.filter((tool) => !permitted.has(tool)))];
459
+ if (disallowed.length > 0) {
460
+ throw new HTTPException(422, {
461
+ message: `first-party MCP tools are disabled by deployment policy: ${disallowed.join(", ")}. Allowed tools: ${allowed.join(", ") || "(none)"}.${hint ? ` ${hint}` : ""}`,
462
+ });
463
+ }
464
+ }
465
+
327
466
  function sessionSpawnDeniedMessage(denial: SessionSpawnDenial): string {
328
467
  if (denial.code === "nested_agent_depth_override_forbidden") {
329
468
  return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
@@ -353,6 +492,21 @@ export type FrozenCreationInitiator = {
353
492
  actor?: Extract<SessionCommandActor, { type: "agent_attempt" }>;
354
493
  };
355
494
 
495
+ /** Attribution is immutable accepted-work data, never the current request principal. */
496
+ export function initiatingHumanForAllowance(
497
+ frozen: Pick<FrozenTurnInitiator, "initiator" | "initiatingHumanSubjectId">,
498
+ ): string | null {
499
+ const subjectId =
500
+ frozen.initiatingHumanSubjectId === undefined
501
+ ? frozen.initiator.kind === "subject"
502
+ ? frozen.initiator.subjectId
503
+ : null
504
+ : frozen.initiatingHumanSubjectId;
505
+ // Older accepted API-key turns used the subject initiator kind. They are
506
+ // still service-funded work, not a human member's allowance.
507
+ return subjectId?.startsWith("api_key:") ? null : subjectId;
508
+ }
509
+
356
510
  function serviceInitiatorForGrant(grant: AccessGrant): {
357
511
  initiator: ServiceTurnInitiator;
358
512
  context: ServiceTurnInitiatorContext;
@@ -432,6 +586,20 @@ export function creationInitiatorForGrant(grant: AccessGrant): FrozenCreationIni
432
586
  if (serviceInitiator) {
433
587
  return serviceInitiator;
434
588
  }
589
+ if (
590
+ grant.principalKind === "service" ||
591
+ grant.principalKind === "api_key" ||
592
+ grant.principalKind === "configured_key"
593
+ ) {
594
+ return {
595
+ initiator: {
596
+ kind: "service",
597
+ subjectId: grant.subjectId,
598
+ ...(grant.subjectLabel ? { label: grant.subjectLabel } : {}),
599
+ },
600
+ context: {},
601
+ };
602
+ }
435
603
  return {
436
604
  initiator: {
437
605
  kind: "subject",
@@ -558,7 +726,7 @@ function validateSessionMcpServersForCreate(
558
726
  }
559
727
  requirePermission(grant, "mcp_servers:attach");
560
728
  const encryptionKey = servers.some((server) => Object.keys(server.headers ?? {}).length > 0)
561
- ? requireVariableSetEncryption(settings)
729
+ ? requireEnvironmentEncryption(settings)
562
730
  : null;
563
731
  const existingIds = new Set(settings.mcpServers.map((server) => server.id));
564
732
  const seenIds = new Set<string>();
@@ -566,7 +734,7 @@ function validateSessionMcpServersForCreate(
566
734
  const dbServers: CreateSessionMcpServerInput[] = [];
567
735
  const metadata: SessionMcpServerMetadata[] = [];
568
736
  for (const server of servers) {
569
- assertHostMcpAuthoritySourceAdmissionEnabled(settings, server.connectionRef);
737
+ assertNativeMcpConnectionRef(server.connectionRef);
570
738
  if (seenIds.has(server.id)) {
571
739
  throw new HTTPException(422, {
572
740
  message: `duplicate session MCP server id: ${server.id}`,
@@ -653,7 +821,7 @@ function validateInheritedSessionMcpServersForCreate(
653
821
  };
654
822
  }
655
823
 
656
- function validateSessionMcpCredentialUpdates(input: {
824
+ export function validateSessionMcpCredentialUpdates(input: {
657
825
  settings: Settings;
658
826
  grant: AccessGrant;
659
827
  session: Session;
@@ -663,7 +831,7 @@ function validateSessionMcpCredentialUpdates(input: {
663
831
  return [];
664
832
  }
665
833
  requirePermission(input.grant, "mcp_servers:attach");
666
- const encryptionKey = requireVariableSetEncryption(input.settings);
834
+ const encryptionKey = requireEnvironmentEncryption(input.settings);
667
835
  const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
668
836
  const seenIds = new Set<string>();
669
837
  const encryptedUpdates = input.updates.map((update) => {
@@ -713,6 +881,27 @@ type AgentChildSessionCreatePresentation = {
713
881
  automaticTitleCandidate?: string | null;
714
882
  };
715
883
 
884
+ /** Trusted entry-point options for creating a session outside the REST body. */
885
+ export type SessionCreateRequestOptions = {
886
+ /**
887
+ * Product surface when the entry point knows it (for example Slack or an
888
+ * in-process embedding host). Omitted derives it from the grant.
889
+ */
890
+ surface?: SessionTurnSurface;
891
+ /**
892
+ * Agent-configuration creator kind (renderer default and default-config
893
+ * policy). Omitted derives `slack` from a Slack surface, else the public API.
894
+ */
895
+ agentConfigCreator?: AgentConfigCreator;
896
+ /**
897
+ * Built-in tools to guarantee on top of the defaults a new session follows,
898
+ * for an entry point whose sessions need their own tools (a chat surface's
899
+ * read tools). Ignored when the request names an explicit built-in list.
900
+ * Trusted entry points only; not part of the public create request.
901
+ */
902
+ firstPartyMcpToolAdditions?: readonly FirstPartyMcpToolName[];
903
+ };
904
+
716
905
  const AGENT_CHILD_AUTOMATIC_TITLE_CONTEXT_KEY = "agentChildAutomaticTitle" as const;
717
906
 
718
907
  /** @internal Exported for the keyed-create repair regression. */
@@ -745,6 +934,13 @@ function automaticTitleForAgentChildCreate(
745
934
  return null;
746
935
  }
747
936
 
937
+ /** Vault IDs are compared as lowercase text in SQL. */
938
+ function lowercaseVaultPolicy(policy: VaultPolicy): VaultPolicy {
939
+ return policy.mode === "all"
940
+ ? { mode: "all" }
941
+ : { mode: "selected", vaultIds: [...new Set(policy.vaultIds.map((id) => id.toLowerCase()))] };
942
+ }
943
+
748
944
  export async function createAndStartSessionWithOutcome(input: {
749
945
  initialAgentLearning?: import("@opengeni/contracts").AgentLearningOverrides | undefined;
750
946
  requestedSessionId?: string;
@@ -760,7 +956,7 @@ export async function createAndStartSessionWithOutcome(input: {
760
956
  captureInitialTurnAuthority?: (tx: Database, sessionId: string, turnId: string) => Promise<void>;
761
957
  /** Internal replay identity; the verified caller must also capture authority.
762
958
  * Supplying selection metadata alone never grants runtime use. */
763
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
959
+
764
960
  /** The custom workspace model was frozen by an earlier accepted boundary or
765
961
  * inherited from an existing session, so retirement must not invalidate it. */
766
962
  retainWorkspaceGatewayModel?: boolean;
@@ -775,6 +971,15 @@ export async function createAndStartSessionWithOutcome(input: {
775
971
  workspaceId: string;
776
972
  visibility?: "user_private" | "workspace_shared";
777
973
  initialMessage: string;
974
+ /** Content-free product surface the create request entered through. */
975
+ surface?: SessionTurnSurface | null;
976
+ /**
977
+ * Live product usage counters: a newly created session (never a replay)
978
+ * counts once, and its initial user message once unless deferred.
979
+ */
980
+ metrics?: ProductUsageMetricsSink | null;
981
+ /** Optional content-free child spans; never a lifecycle dependency. */
982
+ startupObservability?: SessionStartObservability | null;
778
983
  /** Create the session shell without an initial user event/agent turn. */
779
984
  deferInitialTurn?: boolean;
780
985
  modelContext?: string | null;
@@ -829,6 +1034,12 @@ export async function createAndStartSessionWithOutcome(input: {
829
1034
  // Model-visible first-party tool names. Authorization remains controlled by
830
1035
  // firstPartyMcpPermissions and the target resource checks.
831
1036
  firstPartyMcpTools: FirstPartyMcpToolName[];
1037
+ // Frozen agent configuration (migration 0559), already resolved and written
1038
+ // through to tools/firstPartyMcpTools by the caller. Omitted/null = legacy.
1039
+ agentConfig?: ResolvedAgentConfig | null;
1040
+ // Vault narrowing for this session (0739), already intersected with the
1041
+ // agent configuration's policy. Omitted = no narrowing.
1042
+ vaultPolicy?: VaultPolicy;
832
1043
  // Agent-access scope, opaque end-user label, and typed Memory selector
833
1044
  // (migration 0427), already resolved against the parent by the caller.
834
1045
  // Omitted keeps the workspace defaults for internal lifecycle callers.
@@ -838,14 +1049,22 @@ export async function createAndStartSessionWithOutcome(input: {
838
1049
  // Encrypted DB rows plus matching safe metadata for create-time per-session
839
1050
  // MCP servers. Metadata is the only shape emitted in events/responses.
840
1051
  mcpServers?: CreateSessionMcpServerInput[];
1052
+ mcpApprovalPolicies?: Record<string, SessionMcpApprovalPolicy>;
841
1053
  sessionMcpServers?: SessionMcpServerMetadata[];
842
1054
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
1055
+ mcpAccountBindings?: McpConnectionAccountBinding[] | null;
1056
+ /** The creator's account intent, carried by work that continues the first turn. */
1057
+ connectionAccountIntent?: ConnectionAccountIntent | null;
843
1058
  initialPersonalResourceAttachmentIntent?: PersonalResourceAttachmentIntent | null;
844
1059
  xaiProviderAccountAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1;
1060
+ claudeProviderAccountAuthoritySnapshot?: ClaudeProviderAccountAuthoritySnapshotV1;
845
1061
  // The manager session spawning this worker (a worker-signed sessionId claim
846
1062
  // on the creating grant); null for direct API creates and scheduled runs.
847
1063
  // When set, the worker's terminal-for-now transitions wake this parent.
848
1064
  parentSessionId?: string | null;
1065
+ // Deployment code_search policy for freezing a new root session's decision.
1066
+ // Omitted falls back to the policy the process installed at boot.
1067
+ codeSearchDeploymentPolicy?: CodeSearchDeploymentPolicy;
849
1068
  // Workspace-scoped CREATE idempotency key. When present, a double-fire with
850
1069
  // the same key (sequential retry OR concurrent race) collapses to a single
851
1070
  // session. Every caller repairs or re-delivers the winner's one atomic start;
@@ -904,6 +1123,13 @@ export async function createAndStartSessionWithOutcome(input: {
904
1123
  allowNestedAgentDepthIncrease?: boolean;
905
1124
  subjectId?: string | null;
906
1125
  }): Promise<CreateSessionOutcome> {
1126
+ const frozenBundledSkillIds = bundledSkillSelectionForAgentConfig(
1127
+ input.bundledSkillIds,
1128
+ input.agentConfig,
1129
+ );
1130
+ if (frozenBundledSkillIds !== input.bundledSkillIds) {
1131
+ input = { ...input, bundledSkillIds: frozenBundledSkillIds };
1132
+ }
907
1133
  const sessionMetadata = metadataWithTurnExecutionPolicyV1(
908
1134
  {
909
1135
  ...input.metadata,
@@ -965,20 +1191,27 @@ export async function createAndStartSessionWithOutcome(input: {
965
1191
  scope: input.turnExecutionPolicy.providerId.startsWith("organization-")
966
1192
  ? ("organization" as const)
967
1193
  : ("workspace" as const),
968
- providerKind: input.turnExecutionPolicy.providerId.includes("openrouter")
969
- ? ("openrouter" as const)
970
- : ("vercel_gateway" as const),
1194
+ providerKind:
1195
+ CLAUDE_CONNECTION_KINDS.find(
1196
+ (kind) =>
1197
+ claudeProviderId(kind) === input.turnExecutionPolicy.providerId ||
1198
+ claudeProviderId(kind, "workspace") === input.turnExecutionPolicy.providerId,
1199
+ ) ??
1200
+ (input.turnExecutionPolicy.providerId.includes("openrouter")
1201
+ ? ("openrouter" as const)
1202
+ : input.turnExecutionPolicy.providerId.endsWith("-opper")
1203
+ ? ("opper" as const)
1204
+ : ("vercel_gateway" as const)),
971
1205
  upstreamModelId: input.turnExecutionPolicy.upstreamModelId,
972
1206
  };
973
1207
  const active = await lockActiveCustomModelForAdmission(tx, {
974
1208
  accountId: input.accountId,
975
1209
  workspaceId: input.workspaceId,
976
1210
  reference,
1211
+ claudeAuthority: { sessionId },
977
1212
  });
978
1213
  if (!active) {
979
- throw new HTTPException(422, {
980
- message: `model is not available: ${input.model}`,
981
- });
1214
+ throw modelUnavailableHttpException(input.model);
982
1215
  }
983
1216
  }
984
1217
  // Reject an already-stale browser draft before the newly inserted
@@ -1020,59 +1253,76 @@ export async function createAndStartSessionWithOutcome(input: {
1020
1253
  // session or the committed denial atomically; an application-side lookup
1021
1254
  // cannot serialize those two source tables against an older writer.
1022
1255
  if (input.createIdempotencyKey) {
1023
- const keyedResult = await createSessionWithIdempotencyKeyResult(input.db, {
1024
- ...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
1025
- accountId: input.accountId,
1026
- workspaceId: input.workspaceId,
1027
- visibility: input.visibility ?? "workspace_shared",
1028
- initialMessage: input.initialMessage,
1029
- initialModelContext: input.modelContext ?? null,
1030
- resources: input.resources,
1031
- skills: input.skills ?? [],
1032
- bundledSkillIds: input.bundledSkillIds,
1033
- tools: input.tools,
1034
- toolPolicy: input.toolPolicy,
1035
- metadata: sessionMetadata,
1036
- initialAgentLearning: input.initialAgentLearning,
1037
- selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
1038
- ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1039
- ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1040
- createdByActor: input.createdByActor ?? null,
1041
- model: input.model,
1042
- reasoningEffort: input.reasoningEffort,
1043
- latencyMode: input.latencyMode ?? "standard",
1044
- sandboxBackend: input.sandboxBackend,
1045
- variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
1046
- variableSetId: input.variableSets?.at(-1)?.id ?? null,
1047
- rigId: input.rigId ?? null,
1048
- rigVersionId: input.rigVersionId ?? null,
1049
- channelId: input.channelId ?? null,
1050
- firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
1051
- firstPartyMcpTools: input.firstPartyMcpTools,
1052
- instructions: input.instructions ?? null,
1053
- policyRole: input.policyRole ?? null,
1054
- ...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
1055
- ...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
1056
- ...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
1057
- parentSessionId: input.parentSessionId ?? null,
1058
- createIdempotencyKey: input.createIdempotencyKey,
1059
- selectedInstalledSkillIds: input.selectedInstalledSkillIds ?? [],
1060
- sandboxGroupId: input.sandboxGroupId ?? null,
1061
- ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
1062
- mcpServers: input.mcpServers ?? [],
1063
- personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1064
- initialPersonalResourceAttachmentIntent:
1065
- input.initialPersonalResourceAttachmentIntent ?? null,
1066
- ...(input.xaiProviderAccountAuthoritySnapshot
1067
- ? {
1068
- initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
1069
- }
1070
- : {}),
1071
- maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
1072
- allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
1073
- subjectId: input.subjectId ?? null,
1074
- ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
1075
- });
1256
+ const keyedResult = await measureSessionStartPhase(
1257
+ input.startupObservability,
1258
+ "shell_insert",
1259
+ () =>
1260
+ createSessionWithIdempotencyKeyResult(input.db, {
1261
+ ...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
1262
+ accountId: input.accountId,
1263
+ workspaceId: input.workspaceId,
1264
+ visibility: input.visibility ?? "workspace_shared",
1265
+ initialMessage: input.initialMessage,
1266
+ initialModelContext: input.modelContext ?? null,
1267
+ resources: input.resources,
1268
+ skills: input.skills ?? [],
1269
+ bundledSkillIds: input.bundledSkillIds,
1270
+ tools: input.tools,
1271
+ toolPolicy: input.toolPolicy,
1272
+ metadata: sessionMetadata,
1273
+ initialAgentLearning: input.initialAgentLearning,
1274
+
1275
+ ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1276
+ ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1277
+ createdByActor: input.createdByActor ?? null,
1278
+ model: input.model,
1279
+ reasoningEffort: input.reasoningEffort,
1280
+ latencyMode: input.latencyMode ?? "standard",
1281
+ sandboxBackend: input.sandboxBackend,
1282
+ variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
1283
+ variableSetId: input.variableSets?.at(-1)?.id ?? null,
1284
+ rigId: input.rigId ?? null,
1285
+ rigVersionId: input.rigVersionId ?? null,
1286
+ channelId: input.channelId ?? null,
1287
+ firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
1288
+ firstPartyMcpTools: input.firstPartyMcpTools,
1289
+ ...(input.agentConfig !== undefined ? { agentConfig: input.agentConfig } : {}),
1290
+ instructions: input.instructions ?? null,
1291
+ ...(input.vaultPolicy ? { vaultPolicy: input.vaultPolicy } : {}),
1292
+ policyRole: input.policyRole ?? null,
1293
+ ...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
1294
+ ...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
1295
+ ...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
1296
+ parentSessionId: input.parentSessionId ?? null,
1297
+ createIdempotencyKey: input.createIdempotencyKey!,
1298
+ selectedInstalledSkillIds: input.selectedInstalledSkillIds ?? [],
1299
+ sandboxGroupId: input.sandboxGroupId ?? null,
1300
+ ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
1301
+ mcpServers: input.mcpServers ?? [],
1302
+ mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
1303
+ personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1304
+ mcpAccountBindings: input.mcpAccountBindings ?? null,
1305
+ connectionAccountIntent: input.connectionAccountIntent ?? null,
1306
+ initialPersonalResourceAttachmentIntent:
1307
+ input.initialPersonalResourceAttachmentIntent ?? null,
1308
+ ...(input.xaiProviderAccountAuthoritySnapshot
1309
+ ? {
1310
+ initialXaiProviderAccountAuthoritySnapshot:
1311
+ input.xaiProviderAccountAuthoritySnapshot,
1312
+ }
1313
+ : {}),
1314
+ ...(input.claudeProviderAccountAuthoritySnapshot
1315
+ ? {
1316
+ initialClaudeProviderAccountAuthoritySnapshot:
1317
+ input.claudeProviderAccountAuthoritySnapshot,
1318
+ }
1319
+ : {}),
1320
+ maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
1321
+ allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
1322
+ subjectId: input.subjectId ?? null,
1323
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
1324
+ }),
1325
+ );
1076
1326
  if (keyedResult.denied) {
1077
1327
  throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
1078
1328
  }
@@ -1103,10 +1353,13 @@ export async function createAndStartSessionWithOutcome(input: {
1103
1353
  changed: finished.changed,
1104
1354
  };
1105
1355
  }
1356
+ // A keyed shell whose start fails stays durable: a retry with the same key
1357
+ // repairs it with the parameters it was first accepted with.
1106
1358
  const finished = await finishStartSession(
1107
1359
  targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
1108
1360
  keyed,
1109
1361
  );
1362
+ recordCreatedSessionUsage(input);
1110
1363
  return {
1111
1364
  session: finished.session,
1112
1365
  outcome: "created",
@@ -1116,67 +1369,84 @@ export async function createAndStartSessionWithOutcome(input: {
1116
1369
  }
1117
1370
  let session: Session;
1118
1371
  try {
1119
- session = await createSession(input.db, {
1120
- ...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
1121
- accountId: input.accountId,
1122
- workspaceId: input.workspaceId,
1123
- visibility: input.visibility ?? "workspace_shared",
1124
- initialMessage: input.initialMessage,
1125
- initialModelContext: input.modelContext ?? null,
1126
- resources: input.resources,
1127
- skills: input.skills ?? [],
1128
- bundledSkillIds: input.bundledSkillIds,
1129
- tools: input.tools,
1130
- toolPolicy: input.toolPolicy,
1131
- metadata: sessionMetadata,
1132
- initialAgentLearning: input.initialAgentLearning,
1133
- ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1134
- selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
1135
- ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1136
- createdByActor: input.createdByActor ?? null,
1137
- model: input.model,
1138
- reasoningEffort: input.reasoningEffort,
1139
- latencyMode: input.latencyMode ?? "standard",
1140
- sandboxBackend: input.sandboxBackend,
1141
- variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
1142
- variableSetId: input.variableSets?.at(-1)?.id ?? null,
1143
- rigId: input.rigId ?? null,
1144
- rigVersionId: input.rigVersionId ?? null,
1145
- channelId: input.channelId ?? null,
1146
- firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
1147
- firstPartyMcpTools: input.firstPartyMcpTools,
1148
- instructions: input.instructions ?? null,
1149
- policyRole: input.policyRole ?? null,
1150
- ...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
1151
- ...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
1152
- ...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
1153
- parentSessionId: input.parentSessionId ?? null,
1154
- sandboxGroupId: input.sandboxGroupId ?? null,
1155
- ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
1156
- mcpServers: input.mcpServers ?? [],
1157
- personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1158
- initialPersonalResourceAttachmentIntent:
1159
- input.initialPersonalResourceAttachmentIntent ?? null,
1160
- ...(input.xaiProviderAccountAuthoritySnapshot
1161
- ? {
1162
- initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
1163
- }
1164
- : {}),
1165
- maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
1166
- allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
1167
- subjectId: input.subjectId ?? null,
1168
- ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
1169
- });
1372
+ session = await measureSessionStartPhase(input.startupObservability, "shell_insert", () =>
1373
+ createSession(input.db, {
1374
+ ...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
1375
+ accountId: input.accountId,
1376
+ workspaceId: input.workspaceId,
1377
+ visibility: input.visibility ?? "workspace_shared",
1378
+ initialMessage: input.initialMessage,
1379
+ initialModelContext: input.modelContext ?? null,
1380
+ resources: input.resources,
1381
+ skills: input.skills ?? [],
1382
+ bundledSkillIds: input.bundledSkillIds,
1383
+ tools: input.tools,
1384
+ toolPolicy: input.toolPolicy,
1385
+ metadata: sessionMetadata,
1386
+ initialAgentLearning: input.initialAgentLearning,
1387
+ ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1388
+
1389
+ ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1390
+ createdByActor: input.createdByActor ?? null,
1391
+ model: input.model,
1392
+ reasoningEffort: input.reasoningEffort,
1393
+ latencyMode: input.latencyMode ?? "standard",
1394
+ sandboxBackend: input.sandboxBackend,
1395
+ variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
1396
+ variableSetId: input.variableSets?.at(-1)?.id ?? null,
1397
+ rigId: input.rigId ?? null,
1398
+ rigVersionId: input.rigVersionId ?? null,
1399
+ channelId: input.channelId ?? null,
1400
+ firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
1401
+ firstPartyMcpTools: input.firstPartyMcpTools,
1402
+ ...(input.agentConfig !== undefined ? { agentConfig: input.agentConfig } : {}),
1403
+ instructions: input.instructions ?? null,
1404
+ ...(input.vaultPolicy ? { vaultPolicy: input.vaultPolicy } : {}),
1405
+ policyRole: input.policyRole ?? null,
1406
+ ...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
1407
+ ...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
1408
+ ...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
1409
+ parentSessionId: input.parentSessionId ?? null,
1410
+ ...(input.codeSearchDeploymentPolicy
1411
+ ? { codeSearchDeploymentPolicy: input.codeSearchDeploymentPolicy }
1412
+ : {}),
1413
+ sandboxGroupId: input.sandboxGroupId ?? null,
1414
+ ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
1415
+ mcpServers: input.mcpServers ?? [],
1416
+ mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
1417
+ personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1418
+ mcpAccountBindings: input.mcpAccountBindings ?? null,
1419
+ connectionAccountIntent: input.connectionAccountIntent ?? null,
1420
+ initialPersonalResourceAttachmentIntent:
1421
+ input.initialPersonalResourceAttachmentIntent ?? null,
1422
+ ...(input.xaiProviderAccountAuthoritySnapshot
1423
+ ? {
1424
+ initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
1425
+ }
1426
+ : {}),
1427
+ ...(input.claudeProviderAccountAuthoritySnapshot
1428
+ ? {
1429
+ initialClaudeProviderAccountAuthoritySnapshot:
1430
+ input.claudeProviderAccountAuthoritySnapshot,
1431
+ }
1432
+ : {}),
1433
+ maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
1434
+ allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
1435
+ subjectId: input.subjectId ?? null,
1436
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
1437
+ }),
1438
+ );
1170
1439
  } catch (error) {
1171
1440
  if (error instanceof SessionSpawnDeniedDbError) {
1172
1441
  throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(error.denial));
1173
1442
  }
1174
1443
  throw error;
1175
1444
  }
1176
- const finished = await finishStartSession(
1445
+ const finished = await finishNewlyCreatedSession(
1177
1446
  targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
1178
1447
  session,
1179
1448
  );
1449
+ recordCreatedSessionUsage(input);
1180
1450
  return {
1181
1451
  session: finished.session,
1182
1452
  outcome: "created",
@@ -1185,6 +1455,51 @@ export async function createAndStartSessionWithOutcome(input: {
1185
1455
  };
1186
1456
  }
1187
1457
 
1458
+ /**
1459
+ * Start an unkeyed shell this call just inserted. If the start fails before
1460
+ * the first event or turn commits, the caller gets the error and has no way to
1461
+ * resume that session, so the bare shell is discarded instead of lingering as
1462
+ * a queued session nothing runs. A shell that did initialize (including an
1463
+ * outcome-unknown commit) is kept.
1464
+ */
1465
+ async function finishNewlyCreatedSession(
1466
+ input: Parameters<typeof finishStartSession>[0],
1467
+ session: Session,
1468
+ ): Promise<Awaited<ReturnType<typeof finishStartSession>>> {
1469
+ try {
1470
+ return await finishStartSession(input, session);
1471
+ } catch (error) {
1472
+ try {
1473
+ await discardUninitializedSessionShell(input.db, {
1474
+ accountId: session.accountId,
1475
+ workspaceId: session.workspaceId,
1476
+ sessionId: session.id,
1477
+ });
1478
+ } catch {
1479
+ // The original start failure is the actionable error; a shell that
1480
+ // cannot be discarded now is no worse than before this cleanup ran.
1481
+ }
1482
+ throw error;
1483
+ }
1484
+ }
1485
+
1486
+ function recordCreatedSessionUsage(input: {
1487
+ metrics?: ProductUsageMetricsSink | null;
1488
+ surface?: SessionTurnSurface | null;
1489
+ createdBy?: TurnInitiator;
1490
+ parentSessionId?: string | null;
1491
+ deferInitialTurn?: boolean;
1492
+ }): void {
1493
+ recordSessionCreated(input.metrics, {
1494
+ surface: input.surface,
1495
+ createdByKind: input.createdBy?.kind ?? "service",
1496
+ parentSessionId: input.parentSessionId,
1497
+ });
1498
+ if (!input.deferInitialTurn) {
1499
+ recordUserMessageAccepted(input.metrics, { surface: input.surface });
1500
+ }
1501
+ }
1502
+
1188
1503
  /** Backward-compatible entity-returning create path used by existing callers. */
1189
1504
  export async function createAndStartSession(
1190
1505
  input: Parameters<typeof createAndStartSessionWithOutcome>[0],
@@ -1203,6 +1518,7 @@ async function finishStartSession(
1203
1518
  db: Database;
1204
1519
  bus: EventBus;
1205
1520
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
1521
+ startupObservability?: SessionStartObservability | null;
1206
1522
  initialMessage: string;
1207
1523
  captureInitialTurnAuthority?: (
1208
1524
  tx: Database,
@@ -1210,6 +1526,7 @@ async function finishStartSession(
1210
1526
  turnId: string,
1211
1527
  ) => Promise<void>;
1212
1528
  deferInitialTurn?: boolean;
1529
+ surface?: SessionTurnSurface | null;
1213
1530
  modelContext?: string | null;
1214
1531
  resources: ResourceRef[];
1215
1532
  tools: ToolRef[];
@@ -1290,68 +1607,92 @@ async function finishStartSession(
1290
1607
  });
1291
1608
  }
1292
1609
  }
1293
- const started = await initializeSessionStartAtomically(input.db, {
1294
- accountId: session.accountId,
1295
- workspaceId: session.workspaceId,
1296
- sessionId: session.id,
1297
- ...(input.captureInitialTurnAuthority
1298
- ? {
1299
- captureInitialTurnAuthority: (tx: Database, turnId: string) =>
1300
- input.captureInitialTurnAuthority!(tx, session.id, turnId),
1301
- }
1302
- : {}),
1303
- ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
1304
- reasoningEffortFallback: input.reasoningEffort,
1305
- turnExecutionPolicy: input.turnExecutionPolicy,
1306
- createdEventPayload: {
1307
- toolPolicy: input.toolPolicy,
1308
- ...(input.variableSets?.length
1610
+ const started = await measureSessionStartPhase(input.startupObservability, "initialize", () =>
1611
+ initializeSessionStartAtomically(input.db, {
1612
+ accountId: session.accountId,
1613
+ workspaceId: session.workspaceId,
1614
+ sessionId: session.id,
1615
+ ...(input.captureInitialTurnAuthority
1309
1616
  ? {
1310
- variableSetIds: input.variableSets.map((variableSet) => variableSet.id),
1311
- variableSets: input.variableSets,
1312
- // Legacy highest-precedence aliases.
1313
- variableSetId: input.variableSets.at(-1)!.id,
1314
- variableSetName: input.variableSets.at(-1)!.name,
1617
+ captureInitialTurnAuthority: (tx: Database, turnId: string) =>
1618
+ input.captureInitialTurnAuthority!(tx, session.id, turnId),
1315
1619
  }
1316
1620
  : {}),
1317
- ...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
1318
- },
1319
- goal: input.goal
1320
- ? {
1321
- text: input.goal.text,
1322
- ...(input.goal.successCriteria !== undefined
1323
- ? { successCriteria: input.goal.successCriteria }
1324
- : {}),
1325
- ...(input.goal.rootConstraints !== undefined
1326
- ? { rootConstraints: input.goal.rootConstraints }
1327
- : {}),
1328
- ...(input.goal.maxAutoContinuations !== undefined
1329
- ? { maxAutoContinuations: input.goal.maxAutoContinuations }
1330
- : {}),
1331
- ...(input.goal.mutationPolicy !== undefined
1332
- ? { mutationPolicy: input.goal.mutationPolicy }
1333
- : {}),
1334
- }
1335
- : null,
1336
- initialAutomaticTitle: initialAutomaticTitleForSessionStart(
1337
- session,
1338
- input.initialAutomaticTitle,
1621
+ ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
1622
+ reasoningEffortFallback: input.reasoningEffort,
1623
+ turnExecutionPolicy: input.turnExecutionPolicy,
1624
+ surface: input.surface ?? null,
1625
+ createdEventPayload: {
1626
+ toolPolicy: input.toolPolicy,
1627
+ ...(input.variableSets?.length
1628
+ ? {
1629
+ variableSetIds: input.variableSets.map((variableSet) => variableSet.id),
1630
+ variableSets: input.variableSets,
1631
+ // Legacy highest-precedence aliases.
1632
+ variableSetId: input.variableSets.at(-1)!.id,
1633
+ variableSetName: input.variableSets.at(-1)!.name,
1634
+ }
1635
+ : {}),
1636
+ ...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
1637
+ },
1638
+ goal: input.goal
1639
+ ? {
1640
+ text: input.goal.text,
1641
+ ...(input.goal.successCriteria !== undefined
1642
+ ? { successCriteria: input.goal.successCriteria }
1643
+ : {}),
1644
+ ...(input.goal.rootConstraints !== undefined
1645
+ ? { rootConstraints: input.goal.rootConstraints }
1646
+ : {}),
1647
+ ...(input.goal.reportRequirements !== undefined
1648
+ ? { reportRequirements: input.goal.reportRequirements }
1649
+ : {}),
1650
+ ...(input.goal.maxAutoContinuations !== undefined
1651
+ ? { maxAutoContinuations: input.goal.maxAutoContinuations }
1652
+ : {}),
1653
+ ...(input.goal.mutationPolicy !== undefined
1654
+ ? { mutationPolicy: input.goal.mutationPolicy }
1655
+ : {}),
1656
+ }
1657
+ : null,
1658
+ initialAutomaticTitle: initialAutomaticTitleForSessionStart(
1659
+ session,
1660
+ input.initialAutomaticTitle,
1661
+ ),
1662
+ consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
1663
+ rememberNewSessionSelection: input.rememberNewSessionSelection ?? null,
1664
+ deferInitialTurn: input.deferInitialTurn === true,
1665
+ }),
1666
+ );
1667
+ // Initialization committed the initial turn, events and wake outbox together.
1668
+ // Live fanout is not workflow authority: signal from the durable revision
1669
+ // without waiting for NATS, but retain and fully join both notifications.
1670
+ const wakeRevision = started.workflowWakeRevision;
1671
+ const [fanout, wake] = await Promise.allSettled([
1672
+ measureSessionStartPhase(input.startupObservability, "event_fanout", () =>
1673
+ publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events),
1339
1674
  ),
1340
- consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
1341
- rememberNewSessionSelection: input.rememberNewSessionSelection ?? null,
1342
- deferInitialTurn: input.deferInitialTurn === true,
1343
- });
1344
- await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
1345
- if (started.workflowWakeRevision !== null) {
1346
- await input.workflowClient.wakeSessionWorkflow({
1347
- accountId: session.accountId,
1348
- workspaceId: session.workspaceId,
1349
- sessionId: session.id,
1350
- workflowId: started.temporalWorkflowId,
1351
- wakeRevision: started.workflowWakeRevision,
1352
- });
1353
- }
1354
- const persisted = await requireSession(input.db, session.workspaceId, session.id);
1675
+ wakeRevision === null
1676
+ ? Promise.resolve()
1677
+ : measureSessionStartPhase(input.startupObservability, "workflow_wake", () =>
1678
+ input.workflowClient.wakeSessionWorkflow({
1679
+ accountId: session.accountId,
1680
+ workspaceId: session.workspaceId,
1681
+ sessionId: session.id,
1682
+ workflowId: started.temporalWorkflowId,
1683
+ wakeRevision,
1684
+ }),
1685
+ ),
1686
+ ]);
1687
+ // Keep the prior serial error priority without detaching attempt work. Normal
1688
+ // transport failure remains best-effort inside publishDurableSessionEvents.
1689
+ if (fanout.status === "rejected") throw fanout.reason;
1690
+ if (wake.status === "rejected") throw wake.reason;
1691
+ const persisted = await measureSessionStartPhase(
1692
+ input.startupObservability,
1693
+ "session_reload",
1694
+ () => requireSession(input.db, session.workspaceId, session.id),
1695
+ );
1355
1696
  const initialTurnId =
1356
1697
  started.turn?.id ??
1357
1698
  (await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ??
@@ -1366,6 +1707,17 @@ export function workflowIdForSession(sessionId: string): string {
1366
1707
  return `session-${sessionId}`;
1367
1708
  }
1368
1709
 
1710
+ /**
1711
+ * The one 422 for a model that is not selectable in the live catalog (retired,
1712
+ * removed, or never offered). The message keeps its historical text; the typed
1713
+ * cause lets the API envelope add `details.code: "model_unavailable"` so
1714
+ * clients can ask for another model instead of offering a futile retry.
1715
+ */
1716
+ export function modelUnavailableHttpException(model: string): HTTPException {
1717
+ const cause = new ModelUnavailableError(model);
1718
+ return new HTTPException(422, { message: cause.message, cause });
1719
+ }
1720
+
1369
1721
  /**
1370
1722
  * Reject an explicit model that the host does not expose. The set of usable
1371
1723
  * models is the union surfaced by `configuredAllowedModels` (the built-in
@@ -1374,8 +1726,9 @@ export function workflowIdForSession(sessionId: string): string {
1374
1726
  * the API edge with 422 rather than enqueuing a turn the worker can't honor.
1375
1727
  *
1376
1728
  * `model` is the effective value selected by the caller's boundary. Top-level
1377
- * omission defaults to `settings.openaiModel`; child-session omission inherits
1378
- * the worker-signed calling turn before reaching this helper. Centralized here
1729
+ * omission resolves the new-chat default (`resolveDefaultSessionModel`);
1730
+ * child-session omission inherits the worker-signed calling turn before
1731
+ * reaching this helper. Centralized here
1379
1732
  * so every model-carrying choke point
1380
1733
  * (create-session, user-message/turn-accept, queued-turn update, and
1381
1734
  * scheduled-task agentConfig — a scheduled task is a session the worker runs
@@ -1390,6 +1743,18 @@ export function canonicalConfiguredModel(
1390
1743
  return model;
1391
1744
  }
1392
1745
  const canonicalModel = canonicalizeConfiguredModelId(settings, model);
1746
+ if (
1747
+ canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX) &&
1748
+ settings.resolvedCodexModelsJson !== undefined
1749
+ ) {
1750
+ if (
1751
+ settings.codexSubscriptionEnabled &&
1752
+ configuredAllowedModels(withCodexCatalogProvider(settings)).includes(canonicalModel)
1753
+ ) {
1754
+ return canonicalModel;
1755
+ }
1756
+ throw modelUnavailableHttpException(model);
1757
+ }
1393
1758
  if (configuredAllowedModels(settings).includes(canonicalModel)) {
1394
1759
  return canonicalModel;
1395
1760
  }
@@ -1412,7 +1777,7 @@ export function canonicalConfiguredModel(
1412
1777
  ) {
1413
1778
  return canonicalModel;
1414
1779
  }
1415
- throw new HTTPException(422, { message: `model is not available: ${model}` });
1780
+ throw modelUnavailableHttpException(model);
1416
1781
  }
1417
1782
 
1418
1783
  export function assertConfiguredModel(settings: Settings, model: string | null | undefined): void {
@@ -1455,10 +1820,10 @@ export function assertSessionAllowsProductModel(
1455
1820
  * worker's authoritative post-resolution gate would fail. `model` is the
1456
1821
  * EFFECTIVE value the caller is about to persist: pass the explicit value at
1457
1822
  * message/turn-update/scheduled-task edges (omitted inherits an
1458
- * already-validated stored default), but at session CREATION pass
1459
- * `payload.model ?? settings.openaiModel` — an omitted model stamps the
1460
- * deployment default onto the session, and under a restricted policy that
1461
- * default may be exactly the provider the policy exists to block.
1823
+ * already-validated stored default), but at session CREATION pass the
1824
+ * effective model: an omitted model stamps the resolved default onto the
1825
+ * session, and under a restricted policy that default may be exactly the
1826
+ * provider the policy exists to block.
1462
1827
  */
1463
1828
  export async function assertWorkspaceModelPolicyAllows(
1464
1829
  db: Database,
@@ -1481,13 +1846,16 @@ export async function assertWorkspaceModelPolicyAllows(
1481
1846
  const verdict = evaluateWorkspaceModelPolicy(policy, {
1482
1847
  providerId,
1483
1848
  modelId: canonicalModel,
1849
+ chargesCredits: policyChargesCreditsForModel(settings, canonicalModel),
1484
1850
  });
1485
1851
  if (!verdict.allowed) {
1486
1852
  throw new HTTPException(422, {
1487
1853
  message:
1488
- verdict.reason === "provider"
1489
- ? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers`
1490
- : `model "${canonicalModel}" is not allowed by this workspace's model policy`,
1854
+ verdict.reason === "credits"
1855
+ ? `model "${canonicalModel}" uses Opengeni credits, which are turned off in this workspace. Choose a model paid by a subscription or API key, or ask a workspace admin to turn Opengeni credits back on`
1856
+ : verdict.reason === "provider"
1857
+ ? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers`
1858
+ : `model "${canonicalModel}" is not allowed by this workspace's model policy`,
1491
1859
  });
1492
1860
  }
1493
1861
  }
@@ -1547,11 +1915,16 @@ type PostUserMessageTurnInput = {
1547
1915
  clientEventId?: string;
1548
1916
  mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
1549
1917
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
1550
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
1918
+ mcpAccountBindings?: McpConnectionAccountBinding[] | null;
1919
+ /** The sender's account intent, carried by work that continues this turn. */
1920
+ connectionAccountIntent?: ConnectionAccountIntent | null;
1921
+
1551
1922
  captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
1552
1923
  personalResourceAttachment?: PersonalResourceAttachmentIntent;
1553
1924
  delivery?: "send" | "steer";
1554
1925
  origin?: "human" | "operator";
1926
+ /** Content-free product surface the request entered through. */
1927
+ surface?: SessionTurnSurface | null;
1555
1928
  actor?: string;
1556
1929
  actorLabel?: string;
1557
1930
  commandActor?: SessionCommandActor;
@@ -1682,6 +2055,8 @@ export async function postUserMessageTurn(
1682
2055
  const { db, settings, accountId, workspaceId, sessionId } = input;
1683
2056
  const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
1684
2057
  const requestedReasoningEffort = input.reasoningEffort ?? null;
2058
+ // A message sent from a Site page records which Site (and version) on its turn.
2059
+ const siteOrigin = currentSiteSessionOrigin();
1685
2060
  // Reject an explicit per-message model the host does not expose; an omitted
1686
2061
  // model inherits the session's model downstream (always a configured id).
1687
2062
  assertConfiguredModel(settings, requestedModel);
@@ -1751,13 +2126,15 @@ export async function postUserMessageTurn(
1751
2126
  input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
1752
2127
  turnExecutionPolicy: input.turnExecutionPolicy,
1753
2128
  source: input.origin === "operator" ? "api" : "user",
2129
+ surface: input.surface ?? null,
1754
2130
  ...(input.recordAgentRunUsage !== undefined
1755
2131
  ? { recordAgentRunUsage: input.recordAgentRunUsage }
1756
2132
  : {}),
1757
2133
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1758
- ...(input.selectedHostMcpDelegations?.length
1759
- ? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
1760
- : {}),
2134
+ mcpAccountBindings: input.mcpAccountBindings ?? null,
2135
+ connectionAccountIntent: input.connectionAccountIntent ?? null,
2136
+ ...(siteOrigin ? { turnMetadata: { [SITE_ORIGIN_METADATA_KEY]: siteOrigin } } : {}),
2137
+
1761
2138
  ...(input.captureTurnAuthority
1762
2139
  ? { captureTurnAuthority: input.captureTurnAuthority }
1763
2140
  : {}),
@@ -1769,7 +2146,10 @@ export async function postUserMessageTurn(
1769
2146
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1770
2147
  ...(freshWorkspaceCustomModel
1771
2148
  ? {
1772
- beforeFreshPromptCommit: async (tx: Database): Promise<void> => {
2149
+ beforeFreshPromptCommit: async (
2150
+ tx: Database,
2151
+ { claudeProviderAccountAuthoritySnapshot, claudeAcceptedSource },
2152
+ ): Promise<void> => {
1773
2153
  const reference = workspaceCustomModelReference(
1774
2154
  settings,
1775
2155
  freshWorkspaceCustomModel,
@@ -1781,17 +2161,24 @@ export async function postUserMessageTurn(
1781
2161
  accountId,
1782
2162
  workspaceId,
1783
2163
  reference,
2164
+ claudeAuthority: {
2165
+ authoritySnapshot: claudeProviderAccountAuthoritySnapshot,
2166
+ ...(claudeAcceptedSource ? { acceptedSource: claudeAcceptedSource } : {}),
2167
+ },
1784
2168
  });
1785
2169
  if (!active) {
1786
- throw new HTTPException(422, {
1787
- message: `model is not available: ${freshWorkspaceCustomModel}`,
1788
- });
2170
+ throw modelUnavailableHttpException(freshWorkspaceCustomModel);
1789
2171
  }
1790
2172
  },
1791
2173
  }
1792
2174
  : {}),
1793
2175
  controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
1794
2176
  }),
2177
+ undefined,
2178
+ "shared",
2179
+ // Linked-actor capture reauthorizes under the organization-membership
2180
+ // lock; take it before the tenancy fence and the canonical prefix.
2181
+ Boolean(input.captureTurnAuthority),
1795
2182
  ),
1796
2183
  );
1797
2184
  } catch (error) {
@@ -1981,11 +2368,144 @@ async function resolveWorkspaceModelBoundarySettings(
1981
2368
  ).settings;
1982
2369
  }
1983
2370
 
2371
+ /** Explicit recovery, never a new prompt or a Pause/Resume command. */
2372
+ export async function retryFailedSession(
2373
+ deps: ApiRouteDeps,
2374
+ grant: AccessGrant,
2375
+ workspaceId: string,
2376
+ sessionId: string,
2377
+ request: SessionRetryRequest,
2378
+ ): Promise<SessionRetryResponse> {
2379
+ requirePermission(grant, "sessions:control");
2380
+ await requireSessionAuthorization(deps, grant, {
2381
+ sessionId,
2382
+ operation: "session.control",
2383
+ surface: "core",
2384
+ });
2385
+ const replay = await withWorkspaceSubjectSessionActivityRls(
2386
+ deps.db,
2387
+ workspaceId,
2388
+ grant.subjectId,
2389
+ async (db) =>
2390
+ await getSessionRetryReceiptInTransaction(db, {
2391
+ workspaceId,
2392
+ sessionId,
2393
+ subjectId: grant.subjectId,
2394
+ request,
2395
+ }),
2396
+ );
2397
+ if (replay) return replay;
2398
+ const session = await requireSession(deps.db, workspaceId, sessionId);
2399
+ // A retry is a new model request: a reached task usage limit refuses it.
2400
+ await requireSessionUsageLimitAllows(
2401
+ deps.db,
2402
+ { accountId: grant.accountId, workspaceId, sessionId, runtime: true },
2403
+ { sink: deps.observability, boundary: "send" },
2404
+ );
2405
+ const failure = await getSessionEvent(deps.db, workspaceId, request.failureEventId);
2406
+ if (!failure || failure.sessionId !== sessionId)
2407
+ throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "The failure event is unavailable");
2408
+ const turn = failure?.turnId ? await getSessionTurn(deps.db, workspaceId, failure.turnId) : null;
2409
+ const retainedModel = turn?.model ?? session.model;
2410
+ const settings = await resolveWorkspaceModelBoundarySettings(
2411
+ deps,
2412
+ grant,
2413
+ workspaceId,
2414
+ [request.model ?? retainedModel],
2415
+ retainedModel,
2416
+ );
2417
+ const model = canonicalConfiguredModel(settings, request.model ?? retainedModel)!;
2418
+ try {
2419
+ assertSessionAllowsProductModel(session, model);
2420
+ } catch (error) {
2421
+ if (error instanceof CodexCompactionV2ProviderLockedError)
2422
+ throw new HTTPException(422, { message: error.message, cause: error });
2423
+ throw error;
2424
+ }
2425
+ await assertWorkspaceModelPolicyAllows(deps.db, settings, workspaceId, model);
2426
+ const executionPolicy = withDeveloperSetupCredentialRestriction(
2427
+ resolveTurnExecutionPolicyV1(settings, {
2428
+ modelId: model,
2429
+ requestedModelId: request.model ?? null,
2430
+ modelSource: request.model === undefined ? "session" : "explicit",
2431
+ reasoningEffort: request.reasoningEffort ?? turn?.reasoningEffort ?? session.reasoningEffort,
2432
+ reasoningSource: request.reasoningEffort === undefined ? "session" : "explicit",
2433
+ latencyMode: request.latencyMode ?? turn?.latencyMode ?? session.latencyMode,
2434
+ latencyModeSource: request.latencyMode === undefined ? "session" : "explicit",
2435
+ }),
2436
+ { grant, trustedMetadata: [session.metadata, turn?.metadata] },
2437
+ );
2438
+ await requireLimit(
2439
+ { ...deps, settings },
2440
+ {
2441
+ accountId: grant.accountId,
2442
+ workspaceId,
2443
+ initiatingHumanSubjectId: turn
2444
+ ? initiatingHumanForAllowance({
2445
+ initiator: turn.initiator,
2446
+ initiatingHumanSubjectId: await getSessionTurnInitiatingHumanSubjectId(
2447
+ deps.db,
2448
+ workspaceId,
2449
+ turn.id,
2450
+ ),
2451
+ })
2452
+ : null,
2453
+ action: "agent_run:create",
2454
+ quantity: 1,
2455
+ model,
2456
+ ...(turn ? { acceptedTurn: { sessionId: session.id, turnId: turn.id } } : {}),
2457
+ },
2458
+ );
2459
+ const result = await runIdempotentPersistenceTransaction(
2460
+ {
2461
+ stage: "session.retry",
2462
+ eventTypes: ["turn.recovery.requested", "session.status.changed"],
2463
+ maxAttempts: 3,
2464
+ },
2465
+ async () =>
2466
+ await withWorkspaceSubjectSessionActivityRls(
2467
+ deps.db,
2468
+ workspaceId,
2469
+ grant.subjectId,
2470
+ async (db) =>
2471
+ await retryFailedSessionInTransaction(db, {
2472
+ accountId: grant.accountId,
2473
+ workspaceId,
2474
+ sessionId,
2475
+ subjectId: grant.subjectId,
2476
+ request,
2477
+ executionPolicy,
2478
+ }),
2479
+ ),
2480
+ );
2481
+ // Fanout and workflow dispatch are reconstructible after the durable commit.
2482
+ // Never report an admitted retry as failed because one notification is down.
2483
+ void Promise.all([
2484
+ deps.workflowClient.requestSessionWorkflowWakeDispatch(),
2485
+ (async () => {
2486
+ const events = await Promise.all(
2487
+ result.eventIds.map((id) => getSessionEvent(deps.db, workspaceId, id)),
2488
+ );
2489
+ await publishDurableSessionEvents(
2490
+ deps.bus,
2491
+ workspaceId,
2492
+ sessionId,
2493
+ events.filter((event): event is SessionEvent => event !== null),
2494
+ );
2495
+ })(),
2496
+ ]).catch(() => {
2497
+ console.warn("[sessions] retry notification failed; durable recovery remains", {
2498
+ errorCode: "session_retry_notification_failed",
2499
+ });
2500
+ });
2501
+ return { outcome: result.outcome, turnId: result.turnId, failureEventId: result.failureEventId };
2502
+ }
2503
+
1984
2504
  async function withSessionCreateUsageRecording(input: {
1985
2505
  deps: ApiRouteDeps;
1986
2506
  grant: AccessGrant;
1987
2507
  workspaceId: string;
1988
- startMode: "realtime" | undefined;
2508
+ startMode: "realtime" | "standard" | undefined;
1989
2509
  origin: "system" | "user";
1990
2510
  createOutcome: CreateSessionOutcome;
1991
2511
  }): Promise<CreateSessionRequestOutcome> {
@@ -2022,29 +2542,30 @@ async function createSessionForRequestInFileScope(
2022
2542
  rawPayload: unknown,
2023
2543
  authorization?: AccessGrantAuthorization,
2024
2544
  agentChildPresentation?: AgentChildSessionCreatePresentation,
2545
+ requestOptions: SessionCreateRequestOptions = {},
2025
2546
  ): Promise<CreateSessionRequestOutcome> {
2026
2547
  const payload = CreateSessionRequest.parse(rawPayload);
2548
+ // An agent started from a Site page keeps its workers off chosen machines
2549
+ // and other sessions' sandboxes.
2550
+ await assertSiteAgentMayChooseCompute(unresolvedDeps.db, grant, payload);
2551
+ // A tool whose backing workload this deployment does not run is dropped, not
2552
+ // rejected: a client echoing a full catalog must not fail on a deployment fact.
2553
+ if (payload.firstPartyMcpTools) {
2554
+ const unavailable = deploymentUnavailableFirstPartyMcpTools(unresolvedDeps.settings);
2555
+ payload.firstPartyMcpTools = payload.firstPartyMcpTools.filter(
2556
+ (tool) => !unavailable.has(tool),
2557
+ );
2558
+ }
2559
+ // Read before any await: the Site scope is request-local provenance.
2560
+ const surface = resolveTurnSurface({
2561
+ grant,
2562
+ authorization,
2563
+ requested: requestOptions.surface,
2564
+ });
2027
2565
  payload.metadata = sessionCreationMetadata(payload.metadata);
2028
2566
  const creationMetadata = externalCreationMetadata(payload.metadata, authorization, grant);
2029
2567
  const externalBeforeCreateCommit = externalContinuationCommitAuthorizer(authorization);
2030
- const hostSelections = payload.selectedHostMcpDelegations ?? [];
2031
- if (
2032
- hostSelections.length &&
2033
- (!authorization ||
2034
- authorization.grant.accountId !== grant.accountId ||
2035
- authorization.grant.subjectId !== grant.subjectId ||
2036
- authorization?.grant.workspaceId !== workspaceId ||
2037
- !hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
2038
- !hasPermission(grant.permissions, "connections:read") ||
2039
- grant.metadata?.["sessionId"] ||
2040
- payload.startMode === "realtime")
2041
- ) {
2042
- throw new HTTPException(403, {
2043
- message: "Host selection requires a verified direct owner and non-realtime start",
2044
- });
2045
- }
2046
- if (hostSelections.length)
2047
- prepareHostMcpOwnerAuthorization(authorization!, workspaceId, "connections:read");
2568
+
2048
2569
  if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
2049
2570
  throw new HTTPException(422, {
2050
2571
  message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
@@ -2059,11 +2580,10 @@ async function createSessionForRequestInFileScope(
2059
2580
  });
2060
2581
  }
2061
2582
  await requireManagedHumanPrivateSessionCreate(unresolvedDeps, authorization, workspaceId);
2062
- if (payload.sandbox === "shared" || typeof payload.sandbox === "object") {
2063
- throw new HTTPException(422, {
2064
- message: "Only-me sessions require their own sandbox",
2065
- });
2066
- }
2583
+ // Who can read a session and who can use a sandbox are separate: an
2584
+ // Only-me session may join a sandbox group the creator can already see.
2585
+ // Its own records stay scoped to its audience; files on the box are as
2586
+ // shared as the box.
2067
2587
  }
2068
2588
  // Parent linkage and execution-context inheritance come ONLY from the
2069
2589
  // worker-signed sessionId claim. A caller cannot nominate a parent in the
@@ -2087,13 +2607,76 @@ async function createSessionForRequestInFileScope(
2087
2607
  throw error;
2088
2608
  }
2089
2609
  }
2610
+ // a create-time usage limit belongs to a new ROOT task and only a
2611
+ // person or the trusted host may set it. Agents (including children, which
2612
+ // always share their root's limit) are rejected even when the value is null.
2613
+ let initialUsageLimitActor: ReturnType<typeof sessionUsageLimitWriteActorForAccess> | null = null;
2614
+ if (payload.usageLimitMicros !== undefined) {
2615
+ try {
2616
+ if (parentSessionId || !authorization) {
2617
+ throw new SessionUsageLimitHttpError(
2618
+ 403,
2619
+ "session_usage_limit_forbidden",
2620
+ "Only a person or the application's server can set a task usage limit.",
2621
+ );
2622
+ }
2623
+ initialUsageLimitActor = sessionUsageLimitWriteActorForAccess(authorization);
2624
+ } catch (error) {
2625
+ recordSessionUsageLimitEvent(unresolvedDeps.observability, "create", "forbidden");
2626
+ throw error;
2627
+ }
2628
+ }
2629
+ if (
2630
+ payload.usageLimitMicros !== undefined &&
2631
+ payload.usageLimitMicros !== null &&
2632
+ !sessionUsageLimitsProducerEnabled(unresolvedDeps.settings)
2633
+ ) {
2634
+ recordSessionUsageLimitEvent(unresolvedDeps.observability, "create", "disabled");
2635
+ throw new SessionUsageLimitHttpError(
2636
+ 422,
2637
+ "session_usage_limit_disabled",
2638
+ "Usage limits are not enabled on this server.",
2639
+ );
2640
+ }
2641
+ // The trusted server in this process (published Site runtime) may attach a
2642
+ // limit to a root session it creates as its own service principal, so the
2643
+ // limit is in place before the first model call. Requests cannot set it.
2644
+ const hostUsageLimit = currentHostSessionUsageLimit();
2645
+ const hostInitialUsageLimit =
2646
+ payload.usageLimitMicros === undefined &&
2647
+ !parentSessionId &&
2648
+ hostUsageLimit !== undefined &&
2649
+ hostUsageLimit.subjectId === grant.subjectId
2650
+ ? hostUsageLimit
2651
+ : null;
2652
+ if (hostInitialUsageLimit && !sessionUsageLimitsProducerEnabled(unresolvedDeps.settings)) {
2653
+ recordSessionUsageLimitEvent(unresolvedDeps.observability, "create", "disabled");
2654
+ throw new SessionUsageLimitHttpError(
2655
+ 422,
2656
+ "session_usage_limit_disabled",
2657
+ "Usage limits are not enabled on this server.",
2658
+ );
2659
+ }
2660
+ // A task whose usage limit is reached cannot start more inference by
2661
+ // spawning another agent.
2662
+ if (parentSessionId) {
2663
+ await requireSessionUsageLimitAllows(
2664
+ db,
2665
+ { accountId: grant.accountId, workspaceId, sessionId: parentSessionId },
2666
+ { sink: unresolvedDeps.observability, boundary: "child_create" },
2667
+ );
2668
+ }
2090
2669
  const parentSession = parentSessionId ? await getSession(db, workspaceId, parentSessionId) : null;
2091
2670
  if (parentSessionId && !parentSession) {
2092
2671
  throw new HTTPException(404, {
2093
2672
  message: `parent session not found in workspace: ${parentSessionId}`,
2094
2673
  });
2095
2674
  }
2096
- const workspace = await requireWorkspace(db, workspaceId);
2675
+ const workspace = await measureSessionStartPhase(
2676
+ unresolvedDeps.observability,
2677
+ "workspace_read",
2678
+ () => requireWorkspace(db, workspaceId),
2679
+ );
2097
2680
  const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
2098
2681
  const parentAuthority = parentSession
2099
2682
  ? await getSessionAuthorityProjection(db, workspaceId, parentSession.id)
@@ -2150,6 +2733,27 @@ async function createSessionForRequestInFileScope(
2150
2733
  }
2151
2734
  : null,
2152
2735
  });
2736
+ // One frozen agent configuration for this session (null = exact legacy).
2737
+ // Resolved before keyed replay so a retry that resolves differently is a
2738
+ // conflict rather than a silent replay.
2739
+ const agentConfigCreator: AgentConfigCreator =
2740
+ requestOptions.agentConfigCreator ?? (requestOptions.surface === "slack" ? "slack" : "api");
2741
+ const agentResolution = resolveSessionAgentConfigForCreate({
2742
+ settings: unresolvedDeps.settings,
2743
+ creator: agentConfigCreator,
2744
+ request: payload.agent,
2745
+ instructions: payload.instructions,
2746
+ workspaceSettings: workspace.settings,
2747
+ parent: parentSession,
2748
+ goal: payload.goal !== undefined,
2749
+ });
2750
+ const agentConfig = agentResolution.config;
2751
+ if (agentResolution.instructions !== undefined) {
2752
+ payload.instructions = agentResolution.instructions;
2753
+ }
2754
+ // Freeze what the agent configuration implies before keyed replay compares
2755
+ // it: a `"none"` agent omits the bundled guides unless the request lists them.
2756
+ bundledSkillIds = bundledSkillSelectionForAgentConfig(bundledSkillIds, agentConfig);
2153
2757
  const parentCallingTurn =
2154
2758
  parentSession && creationInitiator.actor
2155
2759
  ? await getSessionTurnForAttempt(
@@ -2176,36 +2780,47 @@ async function createSessionForRequestInFileScope(
2176
2780
  ? grant.subjectId
2177
2781
  : null;
2178
2782
  let retainedKeyedShellModel: string | null = null;
2783
+ let retainedKeyedShellReasoningEffort: Session["reasoningEffort"] | null = null;
2179
2784
  if (
2180
2785
  payload.idempotencyKey &&
2181
2786
  (effectiveVisibility !== "user_private" || replayManagedHumanSubjectId !== null)
2182
2787
  ) {
2788
+ const createIdempotencyKey = payload.idempotencyKey;
2183
2789
  try {
2184
- const initializedReplay = await getInitializedSessionCreateReplay(db, {
2185
- bundledSkillIds,
2186
- accountId: grant.accountId,
2187
- workspaceId,
2188
- subjectId: replayManagedHumanSubjectId ?? grant.subjectId,
2189
- ...(replayManagedHumanSubjectId
2190
- ? { activeManagedHumanSubjectId: replayManagedHumanSubjectId }
2191
- : {}),
2192
- createIdempotencyKey: payload.idempotencyKey,
2193
- selectedInstalledSkillIds: payload.installedSkillIds ?? [],
2194
- initialAgentLearning: payload.agentLearning,
2195
- ...sessionScope,
2196
- selectedHostMcpDelegations: hostSelections,
2197
- ...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
2198
- visibility: effectiveVisibility,
2199
- variableSetIds: payload.variableSetIds ?? [],
2200
- initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
2201
- deferInitialTurn: payload.startMode === "realtime",
2202
- });
2203
- if (initializedReplay) {
2204
- if (initializedReplay.outcome === "denied") {
2205
- throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(initializedReplay.denial));
2206
- }
2790
+ const initializedReplay = await measureSessionStartPhase(
2791
+ unresolvedDeps.observability,
2792
+ "idempotent_replay",
2793
+ () =>
2794
+ getInitializedSessionCreateReplay(db, {
2795
+ bundledSkillIds,
2796
+ accountId: grant.accountId,
2797
+ workspaceId,
2798
+ subjectId: replayManagedHumanSubjectId ?? grant.subjectId,
2799
+ ...(replayManagedHumanSubjectId
2800
+ ? { activeManagedHumanSubjectId: replayManagedHumanSubjectId }
2801
+ : {}),
2802
+ createIdempotencyKey,
2803
+ selectedInstalledSkillIds: payload.installedSkillIds ?? [],
2804
+ initialAgentLearning: payload.agentLearning,
2805
+ ...sessionScope,
2806
+
2807
+ ...(payload.requestedSessionId
2808
+ ? { requestedSessionId: payload.requestedSessionId }
2809
+ : {}),
2810
+ visibility: effectiveVisibility,
2811
+ variableSetIds: payload.variableSetIds ?? [],
2812
+ initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
2813
+ deferInitialTurn: payload.startMode === "realtime",
2814
+ agentConfig,
2815
+ }),
2816
+ );
2817
+ if (initializedReplay) {
2818
+ if (initializedReplay.outcome === "denied") {
2819
+ throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(initializedReplay.denial));
2820
+ }
2207
2821
  if (initializedReplay.outcome === "pending") {
2208
2822
  retainedKeyedShellModel = initializedReplay.session.model;
2823
+ retainedKeyedShellReasoningEffort = initializedReplay.session.reasoningEffort;
2209
2824
  } else {
2210
2825
  if (initializedReplay.workflowWakeRevision !== null) {
2211
2826
  await unresolvedDeps.workflowClient.wakeSessionWorkflow({
@@ -2243,12 +2858,17 @@ async function createSessionForRequestInFileScope(
2243
2858
  throw error;
2244
2859
  }
2245
2860
  }
2246
- let settings = await resolveWorkspaceModelBoundarySettings(
2247
- unresolvedDeps,
2248
- grant,
2249
- workspaceId,
2250
- [payload.model],
2251
- retainedKeyedShellModel,
2861
+ let settings = await measureSessionStartPhase(
2862
+ unresolvedDeps.observability,
2863
+ "model_catalog_initial",
2864
+ () =>
2865
+ resolveWorkspaceModelBoundarySettings(
2866
+ unresolvedDeps,
2867
+ grant,
2868
+ workspaceId,
2869
+ [payload.model],
2870
+ retainedKeyedShellModel,
2871
+ ),
2252
2872
  );
2253
2873
  let deps =
2254
2874
  settings === unresolvedDeps.settings
@@ -2266,14 +2886,42 @@ async function createSessionForRequestInFileScope(
2266
2886
  payload.personalResourceAttachment,
2267
2887
  false,
2268
2888
  );
2269
- const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
2889
+ // A top-level create that names no model gets the resolved new-chat default
2890
+ // (saved workspace default, then a usable connected subscription, then the
2891
+ // credits default while the organization holds credits, then the deployment
2892
+ // default). Children still inherit their calling turn, never this default.
2893
+ // A keyed retry of a still-uninitialized shell keeps the default that shell
2894
+ // already persisted instead of resolving again.
2895
+ const resolvedDefault =
2896
+ parentSession || payload.model !== undefined
2897
+ ? null
2898
+ : retainedKeyedShellModel !== null && retainedKeyedShellReasoningEffort !== null
2899
+ ? { model: retainedKeyedShellModel, reasoningEffort: retainedKeyedShellReasoningEffort }
2900
+ : await measureSessionStartPhase(unresolvedDeps.observability, "default_model", () =>
2901
+ resolveDefaultSessionModel(db, settings, {
2902
+ accountId: grant.accountId,
2903
+ workspaceId,
2904
+ subjectId: grant.subjectId,
2905
+ workspaceSettings: workspace.settings,
2906
+ }),
2907
+ );
2908
+ const inheritedModel =
2909
+ parentCallingTurn?.model ??
2910
+ parentSession?.model ??
2911
+ resolvedDefault?.model ??
2912
+ settings.openaiModel;
2270
2913
  const effectiveModelId = payload.model ?? inheritedModel;
2271
- const effectiveCatalogSettings = await resolveWorkspaceModelBoundarySettings(
2272
- deps,
2273
- grant,
2274
- workspaceId,
2275
- [effectiveModelId],
2276
- parentSession ? inheritedModel : null,
2914
+ const effectiveCatalogSettings = await measureSessionStartPhase(
2915
+ unresolvedDeps.observability,
2916
+ "model_catalog_effective",
2917
+ () =>
2918
+ resolveWorkspaceModelBoundarySettings(
2919
+ deps,
2920
+ grant,
2921
+ workspaceId,
2922
+ [effectiveModelId],
2923
+ parentSession ? inheritedModel : null,
2924
+ ),
2277
2925
  );
2278
2926
  if (effectiveCatalogSettings !== settings) {
2279
2927
  deps = {
@@ -2320,6 +2968,15 @@ async function createSessionForRequestInFileScope(
2320
2968
  creationInitiator.actor.turnId,
2321
2969
  )
2322
2970
  : undefined;
2971
+ const claudeProviderAccountAuthoritySnapshot =
2972
+ parentSession && creationInitiator.actor
2973
+ ? await getSessionTurnClaudeProviderAccountAuthoritySnapshot(
2974
+ db,
2975
+ workspaceId,
2976
+ parentSession.id,
2977
+ creationInitiator.actor.turnId,
2978
+ )
2979
+ : undefined;
2323
2980
  const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
2324
2981
  const inheritedPersonalConnectionDelegations =
2325
2982
  connectionDelegationSource.kind === "turn"
@@ -2330,15 +2987,20 @@ async function createSessionForRequestInFileScope(
2330
2987
  connectionDelegationSource.turnId,
2331
2988
  )
2332
2989
  : null;
2333
- const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
2334
- db,
2335
- workspaceId,
2336
- settings,
2337
- inheritedPersonalConnectionDelegations
2338
- ? {
2339
- personalConnectionDelegations: inheritedPersonalConnectionDelegations,
2340
- }
2341
- : { subjectId: grant.subjectId },
2990
+ const capabilityRuntimeSettings = await measureSessionStartPhase(
2991
+ unresolvedDeps.observability,
2992
+ "capability_settings",
2993
+ () =>
2994
+ settingsWithEnabledCapabilityMcpServers(
2995
+ db,
2996
+ workspaceId,
2997
+ settings,
2998
+ inheritedPersonalConnectionDelegations
2999
+ ? {
3000
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
3001
+ }
3002
+ : { subjectId: grant.subjectId },
3003
+ ),
2342
3004
  );
2343
3005
  const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers")
2344
3006
  ? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers)
@@ -2351,6 +3013,27 @@ async function createSessionForRequestInFileScope(
2351
3013
  capabilityRuntimeSettings,
2352
3014
  sessionMcpServers.runtimeServers,
2353
3015
  );
3016
+ const requestedMcpPolicies =
3017
+ payload.mcpApprovalPolicies ?? parentSession?.mcpApprovalPolicies ?? {};
3018
+ const mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy> = {};
3019
+ if (Object.keys(requestedMcpPolicies).length > 0) {
3020
+ requirePermission(grant, "sessions:control");
3021
+ const inheritedServers = await listEnabledMcpCapabilityServers(db, workspaceId);
3022
+ for (const [id, policy] of Object.entries(requestedMcpPolicies)) {
3023
+ const inherited = inheritedServers.find((server) => server.id === id);
3024
+ if (!inherited || sessionMcpServers.runtimeServers.some((server) => server.id === id)) {
3025
+ throw new HTTPException(422, {
3026
+ message: `MCP approval policy must name an enabled inherited capability: ${id}${
3027
+ /^api_(openapi|graphql)_/.test(id)
3028
+ ? "; API Integration tool approval is set at install with autoApprovedTools"
3029
+ : ""
3030
+ }`,
3031
+ });
3032
+ }
3033
+ mcpApprovalPolicies[id] =
3034
+ resolveMcpApprovalRecommendation(policy, inherited.approvalFloor) ?? false;
3035
+ }
3036
+ }
2354
3037
  const resources = normalizeResources(
2355
3038
  hasOwnProperty(rawPayload, "resources")
2356
3039
  ? payload.resources
@@ -2395,6 +3078,11 @@ async function createSessionForRequestInFileScope(
2395
3078
  });
2396
3079
  }
2397
3080
  const toolsProvided = hasOwnProperty(rawPayload, "tools");
3081
+ if (toolsProvided && payload.excludedMcpServerIds !== undefined) {
3082
+ throw new HTTPException(422, {
3083
+ message: "connector exclusions require workspace-default tools",
3084
+ });
3085
+ }
2398
3086
  // Visibility became durable draft state after older clients had already
2399
3087
  // written rows without it. Compare it only when the create request supplied
2400
3088
  // the field explicitly; the parsed schema default must not manufacture a
@@ -2417,6 +3105,9 @@ async function createSessionForRequestInFileScope(
2417
3105
  )
2418
3106
  : parentSession.tools,
2419
3107
  runtimeSettings,
3108
+ ).filter(
3109
+ (tool) =>
3110
+ tool.id === "opengeni" || !parentSession.toolPolicy.excludedMcpServerIds?.includes(tool.id),
2420
3111
  );
2421
3112
  if (toolsProvided) {
2422
3113
  assertToolRefsSubset(
@@ -2434,6 +3125,7 @@ async function createSessionForRequestInFileScope(
2434
3125
  toolPolicy = {
2435
3126
  mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
2436
3127
  inheritedFromSessionId: parentSession.id,
3128
+ ...defaultPolicyExclusions(parentSession.toolPolicy.excludedMcpServerIds),
2437
3129
  };
2438
3130
  }
2439
3131
  } else if (toolsProvided) {
@@ -2448,50 +3140,87 @@ async function createSessionForRequestInFileScope(
2448
3140
  );
2449
3141
  toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
2450
3142
  }
3143
+ if (!parentSession) {
3144
+ selectedTools = withSameRequestSessionMcpServerTools(
3145
+ selectedTools,
3146
+ sessionMcpServers.runtimeServers,
3147
+ );
3148
+ }
3149
+ if (payload.excludedMcpServerIds !== undefined) {
3150
+ if (toolPolicy.mode !== "workspace_default") {
3151
+ throw new HTTPException(403, {
3152
+ message: "connector exclusions require workspace-default tools",
3153
+ });
3154
+ }
3155
+ toolPolicy = {
3156
+ ...toolPolicy,
3157
+ ...defaultPolicyExclusions([
3158
+ ...(toolPolicy.excludedMcpServerIds ?? []),
3159
+ ...payload.excludedMcpServerIds,
3160
+ ]),
3161
+ };
3162
+ }
3163
+ selectedTools = withoutExcludedMcpServers(selectedTools, toolPolicy.excludedMcpServerIds);
3164
+ if (agentConfig) {
3165
+ // Product refs survive a connectors-off configuration: servers attached to
3166
+ // this session, refs the request named, and a parent's explicit refs.
3167
+ const productServerIds = new Set<string>([
3168
+ ...sessionMcpServers.runtimeServers.map((server) => server.id),
3169
+ ...(toolsProvided ? payload.tools.map((tool) => tool.id) : []),
3170
+ ...(parentSession && parentSession.toolPolicy.mode !== "workspace_default"
3171
+ ? parentSession.tools.map((tool) => tool.id)
3172
+ : []),
3173
+ ]);
3174
+ const writeThrough = applySessionAgentConfigWriteThrough({
3175
+ config: agentConfig,
3176
+ firstPartyMcpTools: [],
3177
+ tools: selectedTools,
3178
+ toolPolicy,
3179
+ productServerIds,
3180
+ ...(toolsProvided && agentConfigCreator !== "slack"
3181
+ ? { explicitServerIds: payload.tools.map((tool) => tool.id) }
3182
+ : {}),
3183
+ });
3184
+ selectedTools = writeThrough.tools;
3185
+ toolPolicy = writeThrough.toolPolicy;
3186
+ }
2451
3187
  // The first-party MCP server is attached to EVERY session. Registration is
2452
3188
  // independently intersected with the exact model-visible selection and the
2453
3189
  // tool's permission/target authorization predicate, so attachment alone
2454
3190
  // exposes nothing.
2455
3191
  const tools = withFirstPartyTools(selectedTools, runtimeSettings);
2456
- const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
2457
- runtimeSettings,
2458
- tools,
2459
- grant,
2460
- workspaceId,
2461
- hostSelections,
2462
- authorization,
2463
- );
3192
+
2464
3193
  const captureLinkedAuthority = prepareExternalLinkTurnAdmission(authorization);
2465
- await validateGitHubRepositorySelection(db, workspaceId, resources);
2466
- if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
2467
- throw new HTTPException(503, {
2468
- message: "object storage is not configured",
2469
- });
2470
- }
2471
- await validateFileResources(
2472
- db,
2473
- grant.accountId,
2474
- workspaceId,
2475
- personalResourceSubjectId ?? grant.subjectId,
2476
- resources,
2477
- (authorization || grant.principalKind === "agent_attempt") &&
2478
- (effectiveVisibility === "user_private" ||
2479
- sessionScope.memoryScope === "user" ||
2480
- workspace.kind === "personal")
2481
- ? authorization
3194
+ const { attachmentOwnerContext, variableSets } = await measureSessionStartPhase(
3195
+ unresolvedDeps.observability,
3196
+ "resource_validation",
3197
+ async () => {
3198
+ await validateGitHubRepositorySelection(db, workspaceId, resources);
3199
+ if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
3200
+ throw new HTTPException(503, {
3201
+ message: "object storage is not configured",
3202
+ });
3203
+ }
3204
+ const ownerContext = authorization
2482
3205
  ? await fileOwnerContextForAccess({ db }, authorization, "sessions:create")
2483
- : await fileOwnerContextForAgent({ db }, grant, "sessions:create")
2484
- : undefined,
3206
+ : grant.principalKind === "agent_attempt"
3207
+ ? await fileOwnerContextForAgent({ db }, grant, "sessions:create")
3208
+ : undefined;
3209
+ await validateFileResources(
3210
+ db,
3211
+ grant.accountId,
3212
+ workspaceId,
3213
+ personalResourceSubjectId ?? grant.subjectId,
3214
+ resources,
3215
+ ownerContext,
3216
+ );
3217
+ // Variable Sets were replaced by vaults (RM-1); a session never selects one.
3218
+ if (payload.variableSetIds?.length) throw variableSetsRemovedError("variableSetIds");
3219
+ return { attachmentOwnerContext: ownerContext, variableSets: [] as VariableSet[] };
3220
+ },
2485
3221
  );
2486
- // Every selected Variable Set is independently authorized. Scope does not
2487
- // affect precedence: explicit order is low-to-high and later sets win name
2488
- // collisions.
2489
- const variableSets: VariableSet[] = [];
2490
- for (const variableSetId of payload.variableSetIds ?? []) {
2491
- variableSets.push(
2492
- await validateVariableSetAttachment({ settings, db }, grant, workspaceId, variableSetId),
2493
- );
2494
- }
3222
+ const attachmentOwner =
3223
+ attachmentOwnerContext?.privateFileOwnerSubjectId === grant.subjectId ? grant.subjectId : null;
2495
3224
  // RIG BINDING (M3). Resolve the rig this session rides — a UUID binds that
2496
3225
  // rig, null explicitly opts out, and omission inherits the workspace default
2497
3226
  // (workspaces.default_rig_id) — then FREEZE both the rig id and its currently-
@@ -2503,34 +3232,33 @@ async function createSessionForRequestInFileScope(
2503
3232
  // - A stale workspace-default rig (deleted → FK-nulled, or somehow with no
2504
3233
  // active version) degrades SILENTLY to rig-less: an operator-side default
2505
3234
  // must never brick every create in the workspace.
2506
- const requestedRigId =
2507
- payload.rigId === undefined ? await getWorkspaceDefaultRigId(db, workspaceId) : payload.rigId;
2508
- let frozenRigId: string | null = null;
2509
- let frozenRigVersionId: string | null = null;
2510
- if (requestedRigId) {
2511
- const rig = await getRig(db, grant, requestedRigId);
2512
- if (!rig || !rig.activeVersion) {
2513
- if (payload.rigId) {
2514
- throw new HTTPException(422, {
2515
- message: rig
2516
- ? `rig ${payload.rigId} has no active version to bind`
2517
- : `unknown rigId: ${payload.rigId}`,
2518
- });
2519
- }
2520
- // else: workspace-default fallback that no longer resolves → rig-less.
2521
- } else {
2522
- for (const defaultVariableSetId of new Set(rig.activeVersion.defaultVariableSetIds)) {
2523
- await validateVariableSetAttachment(
2524
- { settings, db },
2525
- grant,
2526
- workspaceId,
2527
- defaultVariableSetId,
2528
- );
3235
+ const { frozenRigId, frozenRigVersionId } = await measureSessionStartPhase(
3236
+ unresolvedDeps.observability,
3237
+ "rig_binding",
3238
+ async () => {
3239
+ const requestedRigId =
3240
+ payload.rigId === undefined
3241
+ ? await getWorkspaceDefaultRigId(db, workspaceId)
3242
+ : payload.rigId;
3243
+ if (!requestedRigId) return { frozenRigId: null, frozenRigVersionId: null };
3244
+ const rig = await getRig(db, grant, requestedRigId);
3245
+ if (!rig || !rig.activeVersion) {
3246
+ if (payload.rigId) {
3247
+ throw new HTTPException(422, {
3248
+ message: rig
3249
+ ? `sandbox environment ${payload.rigId} has no active version to bind`
3250
+ : `unknown rigId: ${payload.rigId}`,
3251
+ });
3252
+ }
3253
+ // else: workspace-default fallback that no longer resolves → rig-less.
3254
+ return { frozenRigId: null, frozenRigVersionId: null };
2529
3255
  }
2530
- frozenRigId = rig.id;
2531
- frozenRigVersionId = rig.activeVersion.id;
2532
- }
2533
- }
3256
+ return {
3257
+ frozenRigId: rig.id as string | null,
3258
+ frozenRigVersionId: rig.activeVersion.id as string | null,
3259
+ };
3260
+ },
3261
+ );
2534
3262
  // CHANNEL FILING. Pure rail organization: a UUID files the session into that
2535
3263
  // workspace channel, omission/null leaves it unfiled (inbox). Resolved
2536
3264
  // workspace-scoped so a foreign channel id can never attach; an explicit
@@ -2546,10 +3274,10 @@ async function createSessionForRequestInFileScope(
2546
3274
  channelId = channel.id;
2547
3275
  }
2548
3276
  // A spawned worker is causally part of the exact turn that created it. Omitted
2549
- // execution policy fields therefore inherit that calling turn rather than the
3277
+ // model and reasoning fields therefore inherit that calling turn rather than the
2550
3278
  // deployment defaults. This is especially important for Codex subscription
2551
3279
  // managers: falling back to the deployment model would silently move a child
2552
- // onto the OpenGeni-credits billing path. Legacy session-bound grants without
3280
+ // onto the Opengeni-credits billing path. Legacy session-bound grants without
2553
3281
  // exact attempt claims fall back to the parent session's persisted defaults.
2554
3282
  const model = canonicalConfiguredModel(settings, effectiveModelId);
2555
3283
  if (model === null || model === undefined) {
@@ -2559,15 +3287,34 @@ async function createSessionForRequestInFileScope(
2559
3287
  // inherited calling-turn model, or deployment default — so the policy must
2560
3288
  // vet that effective value, not just explicit ones (a restricted workspace's
2561
3289
  // inherited/default-model session would otherwise be born blocked).
2562
- await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
3290
+ await measureSessionStartPhase(unresolvedDeps.observability, "model_admission", async () => {
3291
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
3292
+ // Direct creation is a fresh model selection. Child inheritance and keyed
3293
+ // repair preserve the existing accepted-model/authority rules.
3294
+ if (retainedKeyedShellModel === null && !parentSession) {
3295
+ const selections = await resolveCallerWorkspaceModelSelections(db, settings, {
3296
+ accountId: grant.accountId,
3297
+ workspaceId,
3298
+ subjectId: personalResourceSubjectId ?? grant.subjectId,
3299
+ ...(claudeProviderAccountAuthoritySnapshot
3300
+ ? { claudeAuthoritySnapshot: claudeProviderAccountAuthoritySnapshot }
3301
+ : {}),
3302
+ });
3303
+ if (!admissibleWorkspaceModel(selections, model)) {
3304
+ throw new HTTPException(422, { message: `model is not selectable: ${model}` });
3305
+ }
3306
+ }
3307
+ });
2563
3308
  const inheritedReasoningEffort =
2564
3309
  parentCallingTurn?.reasoningEffort ??
2565
3310
  parentSession?.reasoningEffort ??
3311
+ resolvedDefault?.reasoningEffort ??
2566
3312
  settings.openaiReasoningEffort;
2567
- const inheritedLatencyMode =
2568
- parentCallingTurn?.latencyMode ?? parentSession?.latencyMode ?? "standard";
2569
3313
  const reasoningEffort = payload.reasoningEffort ?? inheritedReasoningEffort;
2570
- const latencyMode = payload.latencyMode ?? inheritedLatencyMode;
3314
+ // A fresh session never implicitly opts into a faster, higher-cost tier.
3315
+ // This is a fresh-creation default only: replay/repair preserves persisted
3316
+ // policy, and follow-ups use the recipient session's own frozen settings.
3317
+ const latencyMode = payload.latencyMode ?? "standard";
2571
3318
  if (payload.expectedNewSessionDraftRevision !== undefined && payload.rigId === null) {
2572
3319
  throw new HTTPException(409, {
2573
3320
  message: "The submitted session options are not represented by the new-session draft",
@@ -2584,6 +3331,9 @@ async function createSessionForRequestInFileScope(
2584
3331
  reasoningEffort,
2585
3332
  latencyMode,
2586
3333
  options: {
3334
+ ...(payload.excludedMcpServerIds !== undefined
3335
+ ? { excludedMcpServerIds: payload.excludedMcpServerIds }
3336
+ : {}),
2587
3337
  ...(visibilityProvided ? { visibility: payload.visibility } : {}),
2588
3338
  ...(payload.sandboxBackend ? { sandboxBackend: payload.sandboxBackend } : {}),
2589
3339
  ...(payload.targetSandboxId ? { targetSandboxId: payload.targetSandboxId } : {}),
@@ -2602,34 +3352,33 @@ async function createSessionForRequestInFileScope(
2602
3352
  ...(payload.firstPartyMcpTools
2603
3353
  ? { firstPartyMcpTools: payload.firstPartyMcpTools }
2604
3354
  : {}),
3355
+ ...(payload.agent ? { agent: payload.agent } : {}),
2605
3356
  },
2606
3357
  }
2607
3358
  : null;
2608
3359
  const inheritedFromParent = parentSession !== null;
2609
- const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
2610
- modelId: model,
2611
- requestedModelId: payload.model ?? null,
2612
- modelSource:
2613
- payload.model === undefined
2614
- ? inheritedFromParent
2615
- ? "continuation"
2616
- : "deployment"
2617
- : "explicit",
2618
- reasoningEffort,
2619
- reasoningSource:
2620
- payload.reasoningEffort === undefined
2621
- ? inheritedFromParent
2622
- ? "continuation"
2623
- : "deployment"
2624
- : "explicit",
2625
- latencyMode,
2626
- latencyModeSource:
2627
- payload.latencyMode === undefined
2628
- ? inheritedFromParent
2629
- ? "continuation"
2630
- : "deployment"
2631
- : "explicit",
2632
- });
3360
+ const turnExecutionPolicy = withDeveloperSetupCredentialRestriction(
3361
+ resolveTurnExecutionPolicyV1(settings, {
3362
+ modelId: model,
3363
+ requestedModelId: payload.model ?? null,
3364
+ modelSource:
3365
+ payload.model === undefined
3366
+ ? inheritedFromParent
3367
+ ? "continuation"
3368
+ : "deployment"
3369
+ : "explicit",
3370
+ reasoningEffort,
3371
+ reasoningSource:
3372
+ payload.reasoningEffort === undefined
3373
+ ? inheritedFromParent
3374
+ ? "continuation"
3375
+ : "deployment"
3376
+ : "explicit",
3377
+ latencyMode,
3378
+ latencyModeSource: payload.latencyMode === undefined ? "deployment" : "explicit",
3379
+ }),
3380
+ { grant, authorization, trustedMetadata: [parentSession?.metadata] },
3381
+ );
2633
3382
  // Parent linkage was resolved above, before context validation. A child with
2634
3383
  // no explicit permission override inherits the creating session's effective
2635
3384
  // grant instead of silently expanding to standalone worker defaults.
@@ -2643,6 +3392,7 @@ async function createSessionForRequestInFileScope(
2643
3392
  const parentFirstPartyMcpPermissions = parentSession
2644
3393
  ? [...(parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS)]
2645
3394
  : null;
3395
+ requireDeveloperSetupDelegatedPermissions(turnExecutionPolicy, payload.firstPartyMcpPermissions);
2646
3396
  if (
2647
3397
  parentFirstPartyMcpPermissions &&
2648
3398
  payload.firstPartyMcpPermissions?.some(
@@ -2663,7 +3413,11 @@ async function createSessionForRequestInFileScope(
2663
3413
  ? parentFirstPartyMcpPermissions.filter((permission) =>
2664
3414
  hasPermission(grant.permissions, permission),
2665
3415
  )
2666
- : null);
3416
+ : grant.permissionMode === "explicit"
3417
+ ? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS.filter((permission) =>
3418
+ hasPermission(grant.permissions, permission),
3419
+ )
3420
+ : null);
2667
3421
  if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
2668
3422
  // An empty set would sign an unusable zero-permission token; the default
2669
3423
  // worker set is expressed by omitting the field.
@@ -2672,6 +3426,7 @@ async function createSessionForRequestInFileScope(
2672
3426
  "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set",
2673
3427
  });
2674
3428
  }
3429
+ requireExplicitPermissionDelegation(grant, firstPartyMcpPermissions ?? []);
2675
3430
  for (const permission of firstPartyMcpPermissions ?? []) {
2676
3431
  if (!hasPermission(grant.permissions, permission)) {
2677
3432
  throw new HTTPException(403, {
@@ -2700,20 +3455,14 @@ async function createSessionForRequestInFileScope(
2700
3455
  // top-level omission selects the workspace's exact default catalog (or the
2701
3456
  // deployment default when the workspace has not configured one).
2702
3457
  const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(settings);
2703
- const disallowedFirstPartyMcpTool = payload.firstPartyMcpTools?.find(
2704
- (tool) => !deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
2705
- );
2706
- if (disallowedFirstPartyMcpTool) {
2707
- throw new HTTPException(422, {
2708
- message: `first-party MCP tool is disabled by deployment policy: ${disallowedFirstPartyMcpTool}`,
2709
- });
2710
- }
2711
3458
  const workspaceFirstPartyDefaults = workspaceSessionToolDefaults?.firstPartyMcpTools?.filter(
2712
3459
  (tool) => deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
2713
3460
  );
2714
- const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
3461
+ const resolvedCreatorFirstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
2715
3462
  payload.firstPartyMcpTools,
2716
- parentSession ? parentSession.firstPartyMcpTools : undefined,
3463
+ parentSession
3464
+ ? resolveSessionFirstPartyMcpTools(settings, parentSession, workspace.settings)
3465
+ : undefined,
2717
3466
  workspaceFirstPartyDefaults && !parentSession
2718
3467
  ? {
2719
3468
  ...deploymentFirstPartyMcpToolPolicy,
@@ -2721,6 +3470,41 @@ async function createSessionForRequestInFileScope(
2721
3470
  }
2722
3471
  : deploymentFirstPartyMcpToolPolicy,
2723
3472
  );
3473
+ // Capabilities only narrow the creator's exact legacy selection; "all" keeps it.
3474
+ const followsFirstPartyDefaults =
3475
+ payload.firstPartyMcpTools === undefined &&
3476
+ (!parentSession || parentSession.toolPolicy.firstPartyMode === "workspace_default");
3477
+ // A child following defaults keeps its parent's guaranteed additions; a
3478
+ // top-level session takes them from its trusted entry point.
3479
+ const firstPartyAdditions = followsFirstPartyDefaults
3480
+ ? (parentSession
3481
+ ? (parentSession.toolPolicy.firstPartyAdditions ?? [])
3482
+ : (requestOptions.firstPartyMcpToolAdditions ?? [])
3483
+ ).filter((tool) => deploymentFirstPartyMcpToolPolicy.allowed.includes(tool))
3484
+ : [];
3485
+ if (followsFirstPartyDefaults) {
3486
+ toolPolicy = {
3487
+ ...toolPolicy,
3488
+ firstPartyMode: "workspace_default",
3489
+ ...(firstPartyAdditions.length
3490
+ ? { firstPartyAdditions: [...new Set(firstPartyAdditions)] }
3491
+ : {}),
3492
+ };
3493
+ }
3494
+ const creatorFirstPartyMcpTools = firstPartyAdditions.length
3495
+ ? [...new Set([...resolvedCreatorFirstPartyMcpTools, ...firstPartyAdditions])]
3496
+ : resolvedCreatorFirstPartyMcpTools;
3497
+ const firstPartyMcpTools = applySessionAgentConfigWriteThrough({
3498
+ config: agentConfig,
3499
+ firstPartyMcpTools: creatorFirstPartyMcpTools,
3500
+ // Slack computes its own baseline selection; only a caller's own explicit
3501
+ // list is checked strictly against the capabilities.
3502
+ explicitFirstPartyMcpTools:
3503
+ agentConfigCreator === "slack" ? undefined : payload.firstPartyMcpTools,
3504
+ tools: [],
3505
+ toolPolicy,
3506
+ productServerIds: [],
3507
+ }).firstPartyMcpTools;
2724
3508
  const googleDrivePublicationEnabled =
2725
3509
  firstPartyMcpTools.includes("editable_artifact_export") &&
2726
3510
  firstPartyMcpTools.includes("editable_artifact_export_status") &&
@@ -2730,17 +3514,32 @@ async function createSessionForRequestInFileScope(
2730
3514
  const atlassianEnabled =
2731
3515
  firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
2732
3516
  (!firstPartyMcpPermissions?.length || firstPartyMcpPermissions.includes("connections:read"));
2733
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
2734
- db,
2735
- workspaceId,
2736
- settings: runtimeSettings,
2737
- tools,
2738
- resources,
2739
- source: connectionDelegationSource,
2740
- authoritySelections: payload.connectionAuthorities,
2741
- googleDrivePublicationEnabled,
2742
- atlassianEnabled,
3517
+ // Creation's stored tool snapshot can omit configured workspace defaults.
3518
+ // Freeze initial-turn accounts against the same executable policy as later
3519
+ // messages; explicit selections and exclusions remain exact.
3520
+ const connectionAccountTools = sessionToolsForConnectionAccounts({
3521
+ session: { tools, toolPolicy },
3522
+ runtimeMcpServers: runtimeSettings.mcpServers,
3523
+ defaultMcpServerIds: workspaceSessionToolPolicyDefaultServerIdsFor(
3524
+ capabilityRuntimeSettings.mcpServers,
3525
+ workspace.settings,
3526
+ ),
2743
3527
  });
3528
+ const { personalConnectionDelegations, mcpAccountBindings, connectionAccountIntent } =
3529
+ await measureSessionStartPhase(unresolvedDeps.observability, "connection_freeze", () =>
3530
+ freezeConnectionAccounts({
3531
+ db,
3532
+ accountId: grant.accountId,
3533
+ workspaceId,
3534
+ settings: runtimeSettings,
3535
+ tools: connectionAccountTools,
3536
+ resources,
3537
+ source: connectionDelegationSource,
3538
+ authoritySelections: payload.connectionAccounts,
3539
+ googleDrivePublicationEnabled,
3540
+ atlassianEnabled,
3541
+ }),
3542
+ );
2744
3543
  if (effectiveGoal) {
2745
3544
  const missingGoalTools = ["goal_update", "goal_progress", "goal_complete", "goal_pause"].filter(
2746
3545
  (name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
@@ -2798,27 +3597,10 @@ async function createSessionForRequestInFileScope(
2798
3597
  sandboxId: string;
2799
3598
  workingDir: string | null;
2800
3599
  } | null = null;
2801
- // ENV-AWARE GROUPING: under the CURRENT mechanics the workspace VariableSet is
2802
- // creation-time box state — the box's manifest env is fixed when it is cold-
2803
- // created, and the SDK's provided-session guard rejects any manifest-env delta
2804
- // at attach. A session carrying a DIFFERENT VariableSet than the box it joins
2805
- // is therefore a genuine shared-state conflict TODAY: its first turn on a warm
2806
- // box dies with "Live sandbox sessions cannot change manifest variableSet
2807
- // variables" (proven live, sessions 5aee77e9 + 63d18823). Until the VariableSet
2808
- // is evicted from the manifest (per-exec, like the git token), grouping must be
2809
- // env-aware: the INHERITED default falls back to an own box on mismatch (a
2810
- // credentialed worker spawned from a credential-less manager just works), and
2811
- // an EXPLICIT shared/{groupId} request with a mismatched VariableSet fails
2812
- // fast at create (422) instead of poisoning the session's first turn.
2813
- // The env conflict is a BOX property, so a boxless group is exempt: a
2814
- // backend:"none" session runs in-process with no sandbox, no manifest, and no
2815
- // provided-session attach — no shared box state exists to conflict, and
2816
- // env-differing spawns from such parents shared safely before the env-aware
2817
- // check. They keep sharing (and keep inheriting "none").
2818
- const requestedVariableSetIds = variableSets.map((variableSet) => variableSet.id);
2819
- const variableSetsMatchGroup = (memberVariableSetIds: readonly string[]): boolean =>
2820
- stableJson(memberVariableSetIds) === stableJson(requestedVariableSetIds);
2821
- // RIG-AWARE GROUPING (M3), the exact sibling of the env-aware gate above: the
3600
+ // Variable Sets no longer split boxes (W1 secrets delivery): user secrets are
3601
+ // resolved per command from `{{secret:…}}` references and never ride the box
3602
+ // manifest, so sessions with different selections share a box safely.
3603
+ // RIG-AWARE GROUPING (M3): the
2822
3604
  // box's rig-baked setup/tooling is fixed at cold-create, so a session joining a
2823
3605
  // shared box must ride the SAME frozen rig_version_id. A mismatch is a genuine
2824
3606
  // shared-state conflict (the box was set up for a different rig) — the INHERITED
@@ -2841,7 +3623,6 @@ async function createSessionForRequestInFileScope(
2841
3623
  }
2842
3624
  const parent = parentSession;
2843
3625
  const parentBoxed = parent.sandboxBackend !== "none";
2844
- const variableSetMismatch = parentBoxed && !variableSetsMatchGroup(parent.variableSetIds);
2845
3626
  let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
2846
3627
  if (parentBoxed && !rigMismatch) {
2847
3628
  const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
@@ -2853,16 +3634,13 @@ async function createSessionForRequestInFileScope(
2853
3634
  rigVersionMatchesGroup(memberRigVersionId),
2854
3635
  );
2855
3636
  }
2856
- if (variableSetMismatch || rigMismatch) {
3637
+ if (rigMismatch) {
2857
3638
  if (payload.sandbox === "shared") {
2858
- // The caller explicitly asked to share while carrying a different
2859
- // VariableSet / rig — surface the conflict at create time, not turn time.
2860
- // VariableSet is checked first so its (pre-rig) message is unchanged for
2861
- // the env-only mismatch the existing gate already covered.
3639
+ // The caller explicitly asked to share while carrying a different rig —
3640
+ // surface the conflict at create time, not turn time.
2862
3641
  throw new HTTPException(422, {
2863
- message: variableSetMismatch
2864
- ? "sandbox:'shared' requires the same variableSet / same environment as the creator's box (the box variable set/environment is fixed at creation); omit sandbox or pass 'new' when attaching a different variableSet/environment."
2865
- : "sandbox:'shared' requires the same rig as the creator's box (the box's rig setup is fixed at creation); omit sandbox or pass 'new' when binding a different rig.",
3642
+ message:
3643
+ "sandbox:'shared' requires the same sandbox environment as the creator's box (setup is fixed at creation); omit sandbox or pass 'new' when selecting a different sandbox environment.",
2866
3644
  });
2867
3645
  }
2868
3646
  // Inherited default: deterministic separation on the genuine shared-state
@@ -2892,26 +3670,7 @@ async function createSessionForRequestInFileScope(
2892
3670
  });
2893
3671
  }
2894
3672
  if (member.sandboxBackend !== "none") {
2895
- // Compare against EVERY member, not one arbitrary row: a legacy env-blind
2896
- // group can carry mixed variableSetIds, and an any-member read would make
2897
- // the join verdict nondeterministic. Post-env-aware groups are homogeneous
2898
- // (both join paths enforce equality), so this reads one distinct value in
2899
- // the common case; a mixed legacy group deterministically rejects.
2900
- const memberVariableSetSelections = await listDistinctVariableSetSelectionsInGroup(
2901
- db,
2902
- workspaceId,
2903
- sandboxChoice.groupId,
2904
- );
2905
- if (
2906
- !memberVariableSetSelections.every((memberVariableSetIds) =>
2907
- variableSetsMatchGroup(memberVariableSetIds),
2908
- )
2909
- ) {
2910
- throw new HTTPException(422, {
2911
- message: `sandbox group ${sandboxChoice.groupId} runs a different variableSet / different environment (the box variable set/environment is fixed at creation); create with the group's variableSet/environment or omit sandbox for an own box.`,
2912
- });
2913
- }
2914
- // Same deterministic all-members check for the frozen rig version (M3): the
3673
+ // Deterministic all-members check for the frozen rig version (M3): the
2915
3674
  // box's rig setup is fixed at creation, so every member must ride the rig
2916
3675
  // this create resolved (or the group is rig-less and so is this create).
2917
3676
  const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
@@ -2925,13 +3684,25 @@ async function createSessionForRequestInFileScope(
2925
3684
  )
2926
3685
  ) {
2927
3686
  throw new HTTPException(422, {
2928
- message: `sandbox group ${sandboxChoice.groupId} runs a different rig (the box's rig setup is fixed at creation); create with the group's rig or omit sandbox for an own box.`,
3687
+ message: `sandbox group ${sandboxChoice.groupId} uses a different sandbox environment (setup is fixed at creation); select the group's sandbox environment or omit sandbox for a separate box.`,
2929
3688
  });
2930
3689
  }
2931
3690
  }
2932
3691
  sandboxGroupId = sandboxChoice.groupId;
2933
3692
  inheritedBackend = member.sandboxBackend;
2934
3693
  inheritedSandboxOs = member.sandboxOs;
3694
+ // Naming the creator's own group is the explicit spelling of "shared".
3695
+ // The machine route is session-local, so copy the trusted parent's exact
3696
+ // route as the omitted default does; otherwise the child lands on the
3697
+ // group's managed box, a different filesystem from the creator's machine.
3698
+ if (parentSession && parentSession.sandboxGroupId === sandboxChoice.groupId) {
3699
+ inheritedActiveTarget = parentSession.activeSandboxId
3700
+ ? {
3701
+ sandboxId: parentSession.activeSandboxId,
3702
+ workingDir: parentSession.workingDir,
3703
+ }
3704
+ : null;
3705
+ }
2935
3706
  }
2936
3707
  // else "new": leave sandboxGroupId null → own singleton group (group ≡ id).
2937
3708
  // A working dir is only meaningful for a TARGETED machine (it is the chosen
@@ -3034,14 +3805,37 @@ async function createSessionForRequestInFileScope(
3034
3805
  "self-hosted execution runs on a Connected Machine, but no machine was selected or inherited; connect the parent session to a machine or provide machineTarget",
3035
3806
  });
3036
3807
  }
3037
- if (payload.startMode !== "realtime") {
3038
- await requireLimit(deps, {
3039
- accountId: grant.accountId,
3040
- workspaceId,
3041
- action: "agent_run:create",
3042
- quantity: 1,
3043
- model,
3044
- });
3808
+ if (payload.startMode !== "realtime" && !model.startsWith("codex/")) {
3809
+ const frozenCreationInitiator = await measureSessionStartPhase(
3810
+ unresolvedDeps.observability,
3811
+ "initiator_freeze",
3812
+ () =>
3813
+ withWorkspaceSessionActivityRls(db, workspaceId, (scopedDb) =>
3814
+ frozenInitiatorForCommandActor(
3815
+ scopedDb,
3816
+ workspaceId,
3817
+ creationInitiator.actor ??
3818
+ (creationInitiator.initiator?.kind === "service"
3819
+ ? {
3820
+ type: "service",
3821
+ subjectId: creationInitiator.initiator.subjectId,
3822
+ ...(creationInitiator.context ? { context: creationInitiator.context } : {}),
3823
+ }
3824
+ : { type: "human", subjectId: creationInitiator.initiator!.subjectId }),
3825
+ grant.subjectLabel,
3826
+ ),
3827
+ ),
3828
+ );
3829
+ await measureSessionStartPhase(unresolvedDeps.observability, "allowance", () =>
3830
+ requireLimit(deps, {
3831
+ accountId: grant.accountId,
3832
+ workspaceId,
3833
+ initiatingHumanSubjectId: initiatingHumanForAllowance(frozenCreationInitiator),
3834
+ action: "agent_run:create",
3835
+ quantity: 1,
3836
+ model,
3837
+ }),
3838
+ );
3045
3839
  }
3046
3840
  const initialLearning =
3047
3841
  payload.agentLearning && Object.keys(payload.agentLearning).length
@@ -3063,10 +3857,31 @@ async function createSessionForRequestInFileScope(
3063
3857
  ? ("personal" as const)
3064
3858
  : ("workspace" as const);
3065
3859
  const initialLearningActor = initialLearningContext?.actor;
3860
+ const initialUsageLimit =
3861
+ initialUsageLimitActor && payload.usageLimitMicros != null
3862
+ ? { actor: initialUsageLimitActor, limitMicros: payload.usageLimitMicros }
3863
+ : hostInitialUsageLimit
3864
+ ? {
3865
+ actor: { type: "host" as const, subjectId: hostInitialUsageLimit.subjectId },
3866
+ limitMicros: hostInitialUsageLimit.limitMicros,
3867
+ }
3868
+ : null;
3869
+ const applyInitialUsageLimit = initialUsageLimit
3870
+ ? async (tx: Database, sessionId: string) =>
3871
+ await applyInitialSessionUsageLimit(tx, {
3872
+ accountId: grant.accountId,
3873
+ workspaceId,
3874
+ sessionId,
3875
+ limitMicros: initialUsageLimit.limitMicros,
3876
+ actor: initialUsageLimit.actor,
3877
+ producerEnabled: sessionUsageLimitsProducerEnabled(unresolvedDeps.settings),
3878
+ })
3879
+ : null;
3066
3880
  const beforeCreateCommit =
3067
3881
  initialLearning && initialLearningContext && initialLearningActor?.kind === "human"
3068
3882
  ? async (tx: Database, sessionId: string) => {
3069
3883
  await externalBeforeCreateCommit?.(tx);
3884
+ await applyInitialUsageLimit?.(tx, sessionId);
3070
3885
  await saveAgentLearningSettings(
3071
3886
  tx,
3072
3887
  {
@@ -3082,7 +3897,12 @@ async function createSessionForRequestInFileScope(
3082
3897
  },
3083
3898
  );
3084
3899
  }
3085
- : externalBeforeCreateCommit;
3900
+ : applyInitialUsageLimit
3901
+ ? async (tx: Database, sessionId: string) => {
3902
+ await externalBeforeCreateCommit?.(tx);
3903
+ await applyInitialUsageLimit(tx, sessionId);
3904
+ }
3905
+ : externalBeforeCreateCommit;
3086
3906
  let createOutcome: CreateSessionOutcome;
3087
3907
  try {
3088
3908
  createOutcome = await createAndStartSessionWithOutcome({
@@ -3090,10 +3910,16 @@ async function createSessionForRequestInFileScope(
3090
3910
  db,
3091
3911
  bus,
3092
3912
  workflowClient,
3913
+ codeSearchDeploymentPolicy: codeSearchDeploymentPolicy(deps.settings),
3093
3914
  accountId: grant.accountId,
3094
3915
  workspaceId,
3095
3916
  visibility: effectiveVisibility,
3096
3917
  initialMessage: payload.initialMessage ?? "",
3918
+ surface,
3919
+ metrics: unresolvedDeps.observability ?? null,
3920
+ ...(unresolvedDeps.observability
3921
+ ? { startupObservability: unresolvedDeps.observability }
3922
+ : {}),
3097
3923
  deferInitialTurn: payload.startMode === "realtime",
3098
3924
  modelContext: payload.modelContext ?? null,
3099
3925
  resources,
@@ -3118,8 +3944,8 @@ async function createSessionForRequestInFileScope(
3118
3944
  sandboxGroupId,
3119
3945
  metadata: creationMetadata ?? {},
3120
3946
  ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
3121
- selectedHostMcpDelegations: hostSelections,
3122
- ...(captureSelectedHostAuthority || captureLinkedAuthority
3947
+
3948
+ ...(payload.startMode !== "realtime"
3123
3949
  ? {
3124
3950
  captureInitialTurnAuthority: async (
3125
3951
  tx: Database,
@@ -3127,7 +3953,33 @@ async function createSessionForRequestInFileScope(
3127
3953
  turnId: string,
3128
3954
  ) => {
3129
3955
  await captureLinkedAuthority?.(tx, sessionId, turnId);
3130
- await captureSelectedHostAuthority?.(tx, sessionId, turnId);
3956
+ if (model.startsWith("codex/")) {
3957
+ await requireLimit(
3958
+ { ...deps, db: tx },
3959
+ {
3960
+ accountId: grant.accountId,
3961
+ workspaceId,
3962
+ initiatingHumanSubjectId: await getSessionTurnInitiatingHumanSubjectId(
3963
+ tx,
3964
+ workspaceId,
3965
+ turnId,
3966
+ ),
3967
+ action: "agent_run:create",
3968
+ quantity: 1,
3969
+ model,
3970
+ acceptedTurn: { sessionId, turnId },
3971
+ },
3972
+ );
3973
+ }
3974
+ if (attachmentOwner)
3975
+ await acceptSessionFileAttachments(tx, {
3976
+ accountId: grant.accountId,
3977
+ workspaceId,
3978
+ sessionId,
3979
+ turnId,
3980
+ subjectId: attachmentOwner,
3981
+ resources,
3982
+ });
3131
3983
  },
3132
3984
  }
3133
3985
  : {}),
@@ -3149,19 +4001,30 @@ async function createSessionForRequestInFileScope(
3149
4001
  // contracts schema). Persisted on the row; composed system-level at turn
3150
4002
  // time. Not surfaced as an event.
3151
4003
  instructions: payload.instructions ?? null,
4004
+ // Session and agent-config vault narrowing, intersected (AX-2). Ancestor
4005
+ // policies apply again whenever this session resolves a reference.
4006
+ vaultPolicy: intersectVaultPolicies(
4007
+ lowercaseVaultPolicy(payload.vaultPolicy ?? VAULT_POLICY_ALL),
4008
+ lowercaseVaultPolicy(payload.agent?.vaultPolicy ?? VAULT_POLICY_ALL),
4009
+ ),
3152
4010
  policyRole: payload.policyRole ?? null,
3153
4011
  agentAccess: sessionScope.agentAccess,
3154
4012
  scopeSubjectId: sessionScope.scopeSubjectId,
3155
4013
  memoryScope: sessionScope.memoryScope,
3156
4014
  firstPartyMcpPermissions,
3157
4015
  firstPartyMcpTools,
4016
+ agentConfig,
3158
4017
  mcpServers: sessionMcpServers.dbServers,
4018
+ mcpApprovalPolicies,
3159
4019
  sessionMcpServers: sessionMcpServers.metadata,
3160
4020
  personalConnectionDelegations,
4021
+ mcpAccountBindings,
4022
+ connectionAccountIntent,
3161
4023
  initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
3162
4024
  workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
3163
4025
  retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
3164
4026
  ...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
4027
+ ...(claudeProviderAccountAuthoritySnapshot ? { claudeProviderAccountAuthoritySnapshot } : {}),
3165
4028
  parentSessionId,
3166
4029
  createIdempotencyKey: payload.idempotencyKey ?? null,
3167
4030
  selectedInstalledSkillIds,
@@ -3213,18 +4076,7 @@ async function createSessionForRequestInFileScope(
3213
4076
  { message: error.message, cause: error },
3214
4077
  );
3215
4078
  }
3216
- if (error instanceof HostMcpDelegationAuthorityError) {
3217
- throw new HTTPException(403, {
3218
- message: "Host delegation authority unavailable",
3219
- cause: error,
3220
- });
3221
- }
3222
- if (error instanceof HostMcpBindingConflictError) {
3223
- throw new HTTPException(409, {
3224
- message: "Host delegation selection conflicts",
3225
- cause: error,
3226
- });
3227
- }
4079
+
3228
4080
  if (error instanceof AgentCommandAuthorityError) {
3229
4081
  throw new HTTPException(403, { message: error.message });
3230
4082
  }
@@ -3273,95 +4125,21 @@ export async function createSessionForRequest(
3273
4125
  workspaceId: string,
3274
4126
  rawPayload: unknown,
3275
4127
  authorization?: AccessGrantAuthorization,
4128
+ requestOptions?: SessionCreateRequestOptions,
3276
4129
  ): Promise<CreateSessionResponse> {
3277
4130
  return (
3278
- await createSessionForRequestWithOutcome(deps, grant, workspaceId, rawPayload, authorization)
4131
+ await createSessionForRequestWithOutcome(
4132
+ deps,
4133
+ grant,
4134
+ workspaceId,
4135
+ rawPayload,
4136
+ authorization,
4137
+ undefined,
4138
+ requestOptions,
4139
+ )
3279
4140
  ).session;
3280
4141
  }
3281
4142
 
3282
- /** Snapshot the selected destination, then recheck live actor, binding and grant
3283
- * inside the transaction which accepts the turn. Neither selection nor a
3284
- * registered binding alone is execution authority. Shared by create/send/steer. */
3285
- function prepareSelectedHostTurnAuthority(
3286
- settings: Settings,
3287
- tools: ToolRef[],
3288
- grant: AccessGrant,
3289
- workspaceId: string,
3290
- selections: HostMcpCreateSelection[],
3291
- authorization?: AccessGrantAuthorization,
3292
- ): ((tx: Database, sessionId: string, turnId: string) => Promise<void>) | undefined {
3293
- if (!selections.length) return undefined;
3294
- if (
3295
- !authorization ||
3296
- authorization.grant.accountId !== grant.accountId ||
3297
- authorization.grant.subjectId !== grant.subjectId ||
3298
- authorization?.grant.workspaceId !== workspaceId ||
3299
- !hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
3300
- !hasPermission(grant.permissions, "connections:read") ||
3301
- grant.metadata?.["sessionId"]
3302
- )
3303
- throw new HTTPException(403, {
3304
- message: "Host selection requires a verified direct owner",
3305
- });
3306
- const beforeCommit = prepareHostMcpOwnerAuthorization(
3307
- authorization,
3308
- workspaceId,
3309
- "connections:read",
3310
- );
3311
- const configs = new Map(
3312
- selections.map((selection) => {
3313
- const configured = settings.mcpServers.find((server) => server.id === selection.serverId);
3314
- if (
3315
- !configured ||
3316
- !tools.some((tool) => tool.kind === "mcp" && tool.id === selection.serverId) ||
3317
- configured.connectionRef?.authoritySource !== "host" ||
3318
- !configured.connectionRef.hostBinding ||
3319
- !configured.url
3320
- )
3321
- throw new HTTPException(422, {
3322
- message: "Host delegation must match a selected configured host server",
3323
- });
3324
- assertHostMcpAuthoritySourceAdmissionEnabled(settings, configured.connectionRef);
3325
- return [selection.serverId, structuredClone(configured)] as const;
3326
- }),
3327
- );
3328
- return async (tx, sessionId, turnId) => {
3329
- const owner = await beforeCommit(tx);
3330
- for (const selection of selections) {
3331
- const delegation = await getHostMcpDelegation(tx, owner, selection.delegationId);
3332
- const binding = delegation ? await getHostMcpBinding(tx, owner, delegation.bindingId) : null;
3333
- const configured = configs.get(selection.serverId)!;
3334
- if (
3335
- !delegation ||
3336
- !binding ||
3337
- delegation.status !== "active" ||
3338
- binding.status !== "active" ||
3339
- delegation.generation !== selection.generation ||
3340
- !hostMcpBindingMatchesSelection(binding, {
3341
- serverId: selection.serverId,
3342
- destinationUrl: configured.url!,
3343
- connectionRef: configured.connectionRef!,
3344
- })
3345
- )
3346
- throw new HTTPException(403, { message: "Host delegation selection changed" });
3347
- try {
3348
- await captureDirectHostMcpAuthority(tx, owner, {
3349
- sessionId,
3350
- turnId,
3351
- delegationId: delegation.id,
3352
- expectedDelegationGeneration: selection.generation,
3353
- });
3354
- } catch (error) {
3355
- if (error instanceof HostMcpDelegationAuthorityError)
3356
- throw new HTTPException(403, { message: "Host delegation authority unavailable" });
3357
- if (error instanceof HostMcpBindingConflictError)
3358
- throw new HTTPException(409, { message: "Host delegation selection conflicts" });
3359
- throw error;
3360
- }
3361
- }
3362
- };
3363
- }
3364
-
3365
4143
  function sessionPromptBoundaryRequestHash(input: {
3366
4144
  delivery: "send" | "steer";
3367
4145
  controlEtag: string | null;
@@ -3376,8 +4154,7 @@ function sessionPromptBoundaryRequestHash(input: {
3376
4154
  latencyMode: "standard" | "priority" | "fast" | null;
3377
4155
  source: "user" | "api";
3378
4156
  mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
3379
- connectionAuthorities?: McpConnectionAuthoritySelection[];
3380
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
4157
+ connectionAccounts?: McpConnectionAccountSelection[];
3381
4158
  personalResourceAttachment?: PersonalResourceAttachmentIntent;
3382
4159
  commandActor: SessionCommandActor;
3383
4160
  }): string {
@@ -3396,10 +4173,7 @@ function sessionPromptBoundaryRequestHash(input: {
3396
4173
  latencyMode: input.latencyMode,
3397
4174
  source: input.source,
3398
4175
  mcpCredentialUpdates: input.mcpCredentialUpdates,
3399
- connectionAuthorities: input.connectionAuthorities ?? [],
3400
- ...(input.selectedHostMcpDelegations?.length
3401
- ? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
3402
- : {}),
4176
+ connectionAccounts: input.connectionAccounts ?? [],
3403
4177
  personalResourceAttachment: input.personalResourceAttachment ?? null,
3404
4178
  ...(input.commandActor.type === "service"
3405
4179
  ? {
@@ -3437,10 +4211,14 @@ async function acceptSessionUserMessageInFileScope(
3437
4211
  latencyMode?: "standard" | "priority" | "fast" | null;
3438
4212
  clientEventId?: string;
3439
4213
  mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
3440
- connectionAuthorities?: McpConnectionAuthoritySelection[];
3441
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
4214
+ connectionAccounts?: McpConnectionAccountSelection[];
3442
4215
  delivery?: "send" | "steer";
3443
4216
  origin?: "human" | "operator";
4217
+ /**
4218
+ * Product surface when the entry point knows it (for example Slack or an
4219
+ * in-process embedding host). Omitted derives it from the grant.
4220
+ */
4221
+ surface?: SessionTurnSurface;
3444
4222
  controlEtag?: string | null;
3445
4223
  expectedDraftRevision?: number | null;
3446
4224
  personalResourceAttachment?: PersonalResourceAttachmentIntent;
@@ -3456,25 +4234,12 @@ async function acceptSessionUserMessageInFileScope(
3456
4234
  replay: boolean;
3457
4235
  }> {
3458
4236
  const { db, bus, workflowClient, objectStorage } = deps;
3459
- const hostSelections = HostMcpCreateSelections.parse(input.selectedHostMcpDelegations ?? []).sort(
3460
- (a, b) => (a.serverId < b.serverId ? -1 : a.serverId > b.serverId ? 1 : 0),
3461
- );
3462
- if (
3463
- hostSelections.length &&
3464
- (!input.authorization ||
3465
- input.authorization.grant.accountId !== grant.accountId ||
3466
- input.authorization.grant.subjectId !== grant.subjectId ||
3467
- input.authorization?.grant.workspaceId !== workspaceId ||
3468
- !hasPermission(input.authorization?.grant.permissions ?? [], "connections:read") ||
3469
- !hasPermission(grant.permissions, "connections:read") ||
3470
- grant.metadata?.["sessionId"])
3471
- )
3472
- throw new HTTPException(403, {
3473
- message: "Host selection requires a verified direct owner",
3474
- });
3475
- if (hostSelections.length)
3476
- prepareHostMcpOwnerAuthorization(input.authorization!, workspaceId, "connections:read");
3477
- const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
4237
+
4238
+ const promptInitiator = creationInitiatorForGrant(grant);
4239
+ const delegatedServiceInitiator =
4240
+ promptInitiator.initiator?.kind === "service"
4241
+ ? { initiator: promptInitiator.initiator, context: promptInitiator.context ?? {} }
4242
+ : null;
3478
4243
  const delivery = input.delivery ?? "send";
3479
4244
  const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
3480
4245
  const commandActor: SessionCommandActor = delegatedServiceInitiator
@@ -3486,7 +4251,7 @@ async function acceptSessionUserMessageInFileScope(
3486
4251
  : {}),
3487
4252
  context: delegatedServiceInitiator.context,
3488
4253
  }
3489
- : { type: "human", subjectId: grant.subjectId };
4254
+ : (promptInitiator.actor ?? { type: "human", subjectId: grant.subjectId });
3490
4255
  await requireSessionAuthorization(deps, grant, {
3491
4256
  sessionId,
3492
4257
  operation: delivery === "steer" ? "session.steer" : "session.append",
@@ -3511,10 +4276,7 @@ async function acceptSessionUserMessageInFileScope(
3511
4276
  latencyMode: input.latencyMode ?? null,
3512
4277
  source,
3513
4278
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
3514
- selectedHostMcpDelegations: hostSelections,
3515
- ...(input.connectionAuthorities
3516
- ? { connectionAuthorities: input.connectionAuthorities }
3517
- : {}),
4279
+ ...(input.connectionAccounts ? { connectionAccounts: input.connectionAccounts } : {}),
3518
4280
  ...(input.personalResourceAttachment
3519
4281
  ? { personalResourceAttachment: input.personalResourceAttachment }
3520
4282
  : {}),
@@ -3568,6 +4330,14 @@ async function acceptSessionUserMessageInFileScope(
3568
4330
  // turn's effective model (a follow-up turn inherits the session's model). A
3569
4331
  // pure read with no side effects.
3570
4332
  const existingSession = await requireSession(db, workspaceId, sessionId);
4333
+ assertSessionIsNotImported(existingSession);
4334
+ // a reached task usage limit refuses a fresh Send/Steer here,
4335
+ // after exact replays returned above and before any queue/interrupt write.
4336
+ await requireSessionUsageLimitAllows(
4337
+ db,
4338
+ { accountId: grant.accountId, workspaceId, sessionId, runtime: true },
4339
+ { sink: deps.observability, boundary: delivery === "steer" ? "steer" : "send" },
4340
+ );
3571
4341
  const settings = await resolveWorkspaceModelBoundarySettings(
3572
4342
  deps,
3573
4343
  grant,
@@ -3601,15 +4371,18 @@ async function acceptSessionUserMessageInFileScope(
3601
4371
  const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
3602
4372
  const sessionLatencyMode = existingSession.latencyMode;
3603
4373
  const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
3604
- const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
3605
- modelId: effectiveModel,
3606
- requestedModelId: input.model ?? null,
3607
- modelSource: input.model == null ? "session" : "explicit",
3608
- reasoningEffort: effectiveReasoningEffort,
3609
- reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
3610
- latencyMode: effectiveLatencyMode,
3611
- latencyModeSource: input.latencyMode == null ? "session" : "explicit",
3612
- });
4374
+ const turnExecutionPolicy = withDeveloperSetupCredentialRestriction(
4375
+ resolveTurnExecutionPolicyV1(settings, {
4376
+ modelId: effectiveModel,
4377
+ requestedModelId: input.model ?? null,
4378
+ modelSource: input.model == null ? "session" : "explicit",
4379
+ reasoningEffort: effectiveReasoningEffort,
4380
+ reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
4381
+ latencyMode: effectiveLatencyMode,
4382
+ latencyModeSource: input.latencyMode == null ? "session" : "explicit",
4383
+ }),
4384
+ { grant, authorization: input.authorization, trustedMetadata: [existingSession.metadata] },
4385
+ );
3613
4386
  if (composerDraftResources) {
3614
4387
  const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
3615
4388
  const unacceptedDraftResource = composerDraftResources.find(
@@ -3627,33 +4400,69 @@ async function acceptSessionUserMessageInFileScope(
3627
4400
  sessionId,
3628
4401
  input.annotations ?? [],
3629
4402
  );
3630
- await requireLimit(deps, {
3631
- accountId: grant.accountId,
4403
+ const admissionActor = commandActor;
4404
+ let frozenAdmissionInitiator = await withWorkspaceSessionActivityRls(
4405
+ db,
3632
4406
  workspaceId,
3633
- action: "agent_run:create",
3634
- quantity: 1,
3635
- model: effectiveModel,
3636
- });
4407
+ (scopedDb) =>
4408
+ frozenInitiatorForCommandActor(scopedDb, workspaceId, admissionActor, grant.subjectLabel),
4409
+ );
4410
+ // Re-submitting a checked-out queue edit retains the original turn's
4411
+ // causal human, just as the locked prompt transaction does.
4412
+ if (delivery === "send" && input.expectedDraftRevision != null) {
4413
+ const draft = await withWorkspaceSubjectSessionActivityRls(
4414
+ db,
4415
+ workspaceId,
4416
+ grant.subjectId,
4417
+ async (scopedDb) =>
4418
+ await getComposerDraftInTransaction(scopedDb, {
4419
+ workspaceId,
4420
+ sessionId,
4421
+ subjectId: grant.subjectId,
4422
+ }),
4423
+ );
4424
+ if (draft?.revision === input.expectedDraftRevision && draft.sourceTurnId) {
4425
+ const sourceTurn = await getSessionTurn(db, workspaceId, draft.sourceTurnId);
4426
+ if (sourceTurn?.sessionId === sessionId) {
4427
+ frozenAdmissionInitiator = {
4428
+ initiator: sourceTurn.initiator,
4429
+ context: sourceTurn.initiatorContext,
4430
+ initiatingHumanSubjectId: await getSessionTurnInitiatingHumanSubjectId(
4431
+ db,
4432
+ workspaceId,
4433
+ sourceTurn.id,
4434
+ ),
4435
+ };
4436
+ }
4437
+ }
4438
+ }
4439
+ if (!effectiveModel.startsWith("codex/")) {
4440
+ await requireLimit(deps, {
4441
+ accountId: grant.accountId,
4442
+ workspaceId,
4443
+ initiatingHumanSubjectId: initiatingHumanForAllowance(frozenAdmissionInitiator),
4444
+ action: "agent_run:create",
4445
+ quantity: 1,
4446
+ model: effectiveModel,
4447
+ });
4448
+ }
3637
4449
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
3638
4450
  throw new HTTPException(503, {
3639
4451
  message: "object storage is not configured",
3640
4452
  });
3641
4453
  }
4454
+ const attachmentOwnerContext = input.authorization
4455
+ ? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
4456
+ : grant.principalKind === "agent_attempt"
4457
+ ? await fileOwnerContextForAgent({ db }, grant, "sessions:control")
4458
+ : undefined;
3642
4459
  await validateFileResources(
3643
4460
  db,
3644
4461
  grant.accountId,
3645
4462
  workspaceId,
3646
4463
  grant.subjectId,
3647
4464
  requestedResources,
3648
- (input.authorization || grant.principalKind === "agent_attempt") &&
3649
- ((await getSessionAuthorityProjection(db, workspaceId, sessionId))?.visibility ===
3650
- "user_private" ||
3651
- existingSession.memoryScope === "user" ||
3652
- (await requireWorkspace(db, workspaceId)).kind === "personal")
3653
- ? input.authorization
3654
- ? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
3655
- : await fileOwnerContextForAgent({ db }, grant, "sessions:control")
3656
- : undefined,
4465
+ attachmentOwnerContext,
3657
4466
  );
3658
4467
  await validateGitHubRepositorySelection(db, workspaceId, [
3659
4468
  ...existingSession.resources,
@@ -3675,7 +4484,7 @@ async function acceptSessionUserMessageInFileScope(
3675
4484
  connectionDelegationSource.turnId,
3676
4485
  )
3677
4486
  : null;
3678
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
4487
+ const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3679
4488
  db,
3680
4489
  workspaceId,
3681
4490
  settings,
@@ -3685,37 +4494,60 @@ async function acceptSessionUserMessageInFileScope(
3685
4494
  }
3686
4495
  : { subjectId: grant.subjectId },
3687
4496
  );
3688
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
3689
- db,
3690
- workspaceId,
3691
- settings: runtimeSettings,
3692
- tools: existingSession.tools,
3693
- resources: [...existingSession.resources, ...requestedResources],
3694
- source: connectionDelegationSource,
3695
- targetSessionId: sessionId,
3696
- googleDrivePublicationEnabled:
3697
- existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
3698
- existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
3699
- (!existingSession.firstPartyMcpPermissions?.length ||
3700
- (existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
3701
- existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
3702
- atlassianEnabled:
3703
- existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
3704
- (!existingSession.firstPartyMcpPermissions?.length ||
3705
- existingSession.firstPartyMcpPermissions.includes("connections:read")),
3706
- ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
4497
+ const runtimeSettings = settingsWithSessionMcpServerMetadata(
4498
+ capabilityRuntimeSettings,
4499
+ existingSession.mcpServers,
4500
+ );
4501
+ // A `workspace_default` session stores only its creation-time tool
4502
+ // snapshot and resolves the current workspace defaults at run time, so the
4503
+ // raw column is not the connector allow-list this follow-up executes with.
4504
+ // Freeze accounts against the same resolved list the composer and the
4505
+ // worker see.
4506
+ const connectionWorkspace = await requireWorkspace(db, workspaceId);
4507
+ const connectionAccountTools = sessionToolsForConnectionAccounts({
4508
+ session: existingSession,
4509
+ runtimeMcpServers: runtimeSettings.mcpServers,
4510
+ defaultMcpServerIds: workspaceSessionToolPolicyDefaultServerIdsFor(
4511
+ capabilityRuntimeSettings.mcpServers,
4512
+ connectionWorkspace.settings,
4513
+ ),
3707
4514
  });
3708
- const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
3709
- settingsWithSessionMcpServerMetadata(runtimeSettings, existingSession.mcpServers),
3710
- existingSession.tools,
3711
- grant,
3712
- workspaceId,
3713
- hostSelections,
3714
- input.authorization,
4515
+ const existingEffectiveFirstPartyTools = resolveSessionFirstPartyMcpTools(
4516
+ settings,
4517
+ existingSession,
4518
+ connectionWorkspace.settings,
3715
4519
  );
3716
- const captureLinkedAuthority = prepareExternalLinkTurnAdmission(input.authorization);
3717
- const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
3718
- await postUserMessageTurn({
4520
+ const { personalConnectionDelegations, mcpAccountBindings, connectionAccountIntent } =
4521
+ await freezeConnectionAccounts({
4522
+ db,
4523
+ accountId: grant.accountId,
4524
+ workspaceId,
4525
+ settings: runtimeSettings,
4526
+ tools: connectionAccountTools,
4527
+ resources: [...existingSession.resources, ...requestedResources],
4528
+ source: connectionDelegationSource,
4529
+ targetSessionId: sessionId,
4530
+ googleDrivePublicationEnabled:
4531
+ existingEffectiveFirstPartyTools.includes("editable_artifact_export") &&
4532
+ existingEffectiveFirstPartyTools.includes("editable_artifact_export_status") &&
4533
+ (!existingSession.firstPartyMcpPermissions?.length ||
4534
+ (existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
4535
+ existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
4536
+ atlassianEnabled:
4537
+ existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
4538
+ (!existingSession.firstPartyMcpPermissions?.length ||
4539
+ existingSession.firstPartyMcpPermissions.includes("connections:read")),
4540
+ ...(input.connectionAccounts ? { authoritySelections: input.connectionAccounts } : {}),
4541
+ });
4542
+
4543
+ const captureLinkedAuthority = prepareExternalLinkTurnAdmission(input.authorization);
4544
+ const surface = resolveTurnSurface({
4545
+ grant,
4546
+ authorization: input.authorization,
4547
+ requested: input.surface,
4548
+ });
4549
+ const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
4550
+ await postUserMessageTurn({
3719
4551
  db,
3720
4552
  bus,
3721
4553
  workflowClient,
@@ -3735,20 +4567,33 @@ async function acceptSessionUserMessageInFileScope(
3735
4567
  turnExecutionPolicy,
3736
4568
  mcpCredentialUpdates,
3737
4569
  personalConnectionDelegations,
3738
- selectedHostMcpDelegations: hostSelections,
3739
- ...(captureSelectedHostAuthority || captureLinkedAuthority
3740
- ? {
3741
- captureTurnAuthority: async (tx: Database, turnId: string) => {
3742
- await captureLinkedAuthority?.(tx, sessionId, turnId);
3743
- await captureSelectedHostAuthority?.(tx, sessionId, turnId);
4570
+ mcpAccountBindings,
4571
+ connectionAccountIntent,
4572
+
4573
+ captureTurnAuthority: async (tx: Database, turnId: string) => {
4574
+ await captureLinkedAuthority?.(tx, sessionId, turnId);
4575
+ // The prompt transaction has frozen the exact authority at this
4576
+ // point. A refusal rolls back both prompt and receipt atomically.
4577
+ if (effectiveModel.startsWith("codex/"))
4578
+ await requireLimit(
4579
+ { ...deps, db: tx },
4580
+ {
4581
+ accountId: grant.accountId,
4582
+ workspaceId,
4583
+ initiatingHumanSubjectId: initiatingHumanForAllowance(frozenAdmissionInitiator),
4584
+ action: "agent_run:create",
4585
+ quantity: 1,
4586
+ model: effectiveModel,
4587
+ acceptedTurn: { sessionId, turnId },
3744
4588
  },
3745
- }
3746
- : {}),
4589
+ );
4590
+ },
3747
4591
  ...(input.personalResourceAttachment
3748
4592
  ? { personalResourceAttachment: input.personalResourceAttachment }
3749
4593
  : {}),
3750
4594
  delivery,
3751
4595
  origin: source === "api" ? "operator" : "human",
4596
+ surface,
3752
4597
  actor: grant.subjectId,
3753
4598
  ...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
3754
4599
  commandActor,
@@ -3763,6 +4608,7 @@ async function acceptSessionUserMessageInFileScope(
3763
4608
  ? { schedulePostCommit: deps.schedulePromptPostCommit }
3764
4609
  : {}),
3765
4610
  });
4611
+ if (!replay) recordUserMessageAccepted(deps.observability, { surface });
3766
4612
  return {
3767
4613
  accepted,
3768
4614
  turn,
@@ -3880,6 +4726,131 @@ export async function updateSessionTitle(
3880
4726
  };
3881
4727
  }
3882
4728
 
4729
+ /**
4730
+ * Set or clear the session's keep-live exemption from the idle-session archive.
4731
+ * Archived sessions are read-only and reject the change.
4732
+ */
4733
+ export async function setSessionRetention(
4734
+ deps: {
4735
+ db: Database;
4736
+ sessionAuthorization?: SessionAuthorizationPort | null;
4737
+ },
4738
+ grant: AccessGrant,
4739
+ sessionId: string,
4740
+ keepLive: boolean,
4741
+ ): Promise<{
4742
+ result: SetSessionKeepLiveResult;
4743
+ relatedSessionAccess: "target" | "root";
4744
+ }> {
4745
+ const authorization = await requireSessionAuthorization(deps, grant, {
4746
+ sessionId,
4747
+ operation: "session.retention.write",
4748
+ surface: "core",
4749
+ });
4750
+ const result = await setSessionKeepLive(deps.db, {
4751
+ workspaceId: grant.workspaceId,
4752
+ sessionId,
4753
+ keepLive,
4754
+ });
4755
+ return { result, relatedSessionAccess: authorization?.relatedSessionAccess ?? "root" };
4756
+ }
4757
+
4758
+ /** Change future defaults without accepting a prompt or touching accepted work. */
4759
+ export async function setSessionModel(
4760
+ deps: Pick<
4761
+ ApiRouteDeps,
4762
+ "db" | "bus" | "settings" | "catalogSourceSettings" | "sessionAuthorization"
4763
+ >,
4764
+ grant: AccessGrant,
4765
+ sessionId: string,
4766
+ request: SetSessionModelRequest,
4767
+ surface: SessionAuthorizationSurface = "core",
4768
+ ) {
4769
+ requirePermission(grant, "sessions:control");
4770
+ const input = SetSessionModelRequest.parse(request);
4771
+ const authorization = await requireSessionAuthorization(deps, grant, {
4772
+ sessionId,
4773
+ operation: "session.model.write",
4774
+ surface,
4775
+ });
4776
+ const settings = await resolveWorkspaceModelBoundarySettings(deps, grant, grant.workspaceId, [
4777
+ input.model,
4778
+ ]);
4779
+ const model = canonicalConfiguredModel(settings, input.model)!;
4780
+ await assertWorkspaceModelPolicyAllows(deps.db, settings, grant.workspaceId, model);
4781
+ const caller = authorization?.actor;
4782
+ const service = serviceInitiatorForGrant(grant);
4783
+ const actor: SessionCommandActor =
4784
+ caller?.kind === "agent_attempt"
4785
+ ? {
4786
+ type: "agent_attempt",
4787
+ sessionId: caller.callerSessionId,
4788
+ turnId: caller.turnId,
4789
+ attemptId: caller.attemptId,
4790
+ executionGeneration: caller.executionGeneration,
4791
+ }
4792
+ : service
4793
+ ? { type: "service", subjectId: service.initiator.subjectId, context: service.context }
4794
+ : { type: "human", subjectId: grant.subjectId };
4795
+ const result = await runIdempotentPersistenceTransaction(
4796
+ {
4797
+ stage: "session.model_settings",
4798
+ eventTypes: ["session.model_settings.updated"],
4799
+ maxAttempts: 3,
4800
+ },
4801
+ async () => {
4802
+ const write = async (tx: Parameters<typeof setSessionModelInTransaction>[0]) =>
4803
+ await setSessionModelInTransaction(tx, {
4804
+ accountId: grant.accountId,
4805
+ workspaceId: grant.workspaceId,
4806
+ sessionId,
4807
+ actor,
4808
+ operationKey: input.idempotencyKey,
4809
+ model,
4810
+ reasoningEffort: input.reasoningEffort,
4811
+ validate: (session) => {
4812
+ try {
4813
+ assertSessionAllowsProductModel(
4814
+ {
4815
+ codexCompactionMode:
4816
+ session.codexCompactionMode as Session["codexCompactionMode"],
4817
+ },
4818
+ model,
4819
+ );
4820
+ // Validate fresh selection/provider and preserved latency exactly as
4821
+ // prompt admission does; never silently clamp requested settings.
4822
+ resolveTurnExecutionPolicyV1(settings, {
4823
+ modelId: model,
4824
+ requestedModelId: input.model,
4825
+ modelSource: "explicit",
4826
+ reasoningEffort: input.reasoningEffort,
4827
+ reasoningSource: "explicit",
4828
+ latencyMode: session.latencyMode as Session["latencyMode"],
4829
+ latencyModeSource: "session",
4830
+ });
4831
+ } catch (error) {
4832
+ throw new HTTPException(422, {
4833
+ message: error instanceof Error ? error.message : "Invalid model settings",
4834
+ cause: error,
4835
+ });
4836
+ }
4837
+ },
4838
+ });
4839
+ return actor.type === "agent_attempt"
4840
+ ? await withWorkspaceSessionActivityRls(deps.db, grant.workspaceId, write)
4841
+ : await withWorkspaceSubjectSessionActivityRls(
4842
+ deps.db,
4843
+ grant.workspaceId,
4844
+ grant.subjectId,
4845
+ write,
4846
+ );
4847
+ },
4848
+ );
4849
+ const event = await getSessionEvent(deps.db, grant.workspaceId, result.eventId);
4850
+ if (event) await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, [event]);
4851
+ return { sessionId, ...result, effectiveFrom: "future_turns" as const };
4852
+ }
4853
+
3883
4854
  /**
3884
4855
  * Update one existing session MCP server's approval policy. The database
3885
4856
  * serializes this write with attempt claim under the session lock: an already
@@ -3905,7 +4876,7 @@ export async function updateSessionMcpApprovalPolicy(
3905
4876
  });
3906
4877
  requirePermission(grant, "sessions:control");
3907
4878
 
3908
- const outcome: { server?: SessionMcpServerMetadata } = {};
4879
+ const outcome: { server?: SessionMcpApprovalPolicyTarget } = {};
3909
4880
  const events = await appendSessionEventsWithLockedSessionUpdate(
3910
4881
  deps.db,
3911
4882
  grant.workspaceId,
@@ -3975,7 +4946,17 @@ function toolPolicyAuditSnapshot(
3975
4946
  const toolRefs = allToolRefs.slice(0, maxToolPolicyAuditRefs);
3976
4947
  return {
3977
4948
  mode: policy.mode,
4949
+ ...(policy.firstPartyMode ? { firstPartyMode: policy.firstPartyMode } : {}),
4950
+ ...(policy.firstPartyAdditions?.length
4951
+ ? { firstPartyAdditions: [...policy.firstPartyAdditions] }
4952
+ : {}),
3978
4953
  inheritedFromSessionId: policy.inheritedFromSessionId,
4954
+ ...(policy.excludedMcpServerIds?.length
4955
+ ? {
4956
+ excludedMcpServerIds: policy.excludedMcpServerIds.slice(0, maxToolPolicyAuditRefs),
4957
+ excludedMcpServerCount: policy.excludedMcpServerIds.length,
4958
+ }
4959
+ : {}),
3979
4960
  // IDs only: no MCP URLs, names, headers, credentials, schemas, or args.
3980
4961
  toolIds: [...toolRefs]
3981
4962
  .sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`))
@@ -3984,7 +4965,37 @@ function toolPolicyAuditSnapshot(
3984
4965
  toolCount: allToolRefs.length,
3985
4966
  firstPartyMcpTools: [...firstPartyMcpTools].sort(),
3986
4967
  firstPartyMcpToolCount: firstPartyMcpTools.length,
3987
- truncated: allToolRefs.length > toolRefs.length,
4968
+ truncated:
4969
+ allToolRefs.length > toolRefs.length ||
4970
+ (policy.excludedMcpServerIds?.length ?? 0) > maxToolPolicyAuditRefs,
4971
+ };
4972
+ }
4973
+
4974
+ /** Exact parent authority shared by tool-policy and agent-configuration writes. */
4975
+ function sessionToolPolicyCeiling(
4976
+ session: Session,
4977
+ settings: Settings,
4978
+ runtimeSettings: Settings,
4979
+ workspaceDefaults: ReturnType<typeof resolveWorkspaceSessionToolDefaults>,
4980
+ ): { tools: ToolRef[]; firstPartyMcpTools: FirstPartyMcpToolName[] } {
4981
+ return {
4982
+ tools: withFirstPartyTools(
4983
+ session.toolPolicy.mode === "workspace_default"
4984
+ ? withWorkspaceDefaultMcpTools(
4985
+ availableToolRefs(session.tools, runtimeSettings),
4986
+ settings,
4987
+ runtimeSettings,
4988
+ workspaceDefaults,
4989
+ )
4990
+ : session.tools,
4991
+ runtimeSettings,
4992
+ ).filter(
4993
+ (tool) =>
4994
+ tool.id === "opengeni" || !session.toolPolicy.excludedMcpServerIds?.includes(tool.id),
4995
+ ),
4996
+ firstPartyMcpTools: resolveSessionFirstPartyMcpTools(settings, session, {
4997
+ sessionToolDefaults: workspaceDefaults,
4998
+ }),
3988
4999
  };
3989
5000
  }
3990
5001
 
@@ -4028,9 +5039,31 @@ export async function updateSessionToolPolicy(
4028
5039
  );
4029
5040
  const explicitRequest = request.mode === "workspace_default" ? null : request;
4030
5041
  const requestedMode = explicitRequest ? "explicit" : "workspace_default";
5042
+ const connectorOnlyEdit =
5043
+ request.mode === "workspace_default" && request.excludedMcpServerIds !== undefined;
5044
+ const requestedExclusions =
5045
+ request.mode === "workspace_default" ? (request.excludedMcpServerIds ?? []) : [];
4031
5046
  const explicitRequestedTools = explicitRequest
4032
5047
  ? (() => {
4033
- const validatedTools = validateToolRefs(explicitRequest.tools, runtimeSettings);
5048
+ // A disconnected stored selection remains policy truth, not executable
5049
+ // authority. Preserve exact existing refs while validating every new or
5050
+ // changed ref against the current runtime registry.
5051
+ const availableIds = new Set(runtimeSettings.mcpServers.map((server) => server.id));
5052
+ const retainedUnavailableRefs = explicitRequest.tools.filter(
5053
+ (tool) =>
5054
+ !availableIds.has(tool.id) &&
5055
+ existingSession.tools.some((existing) => stableJson(existing) === stableJson(tool)),
5056
+ );
5057
+ const retainedIds = new Set(retainedUnavailableRefs.map((tool) => tool.id));
5058
+ const validatedCurrentRefs = validateToolRefs(
5059
+ explicitRequest.tools.filter((tool) => !retainedIds.has(tool.id)),
5060
+ runtimeSettings,
5061
+ );
5062
+ const validatedTools = explicitRequest.tools.filter(
5063
+ (tool) =>
5064
+ retainedUnavailableRefs.includes(tool) ||
5065
+ validatedCurrentRefs.some((validated) => validated.id === tool.id),
5066
+ );
4034
5067
  const validatedIds = new Set(validatedTools.map((tool) => `${tool.kind}:${tool.id}`));
4035
5068
  const unknown = explicitRequest.tools.find(
4036
5069
  (tool) => !validatedIds.has(`${tool.kind}:${tool.id}`),
@@ -4043,18 +5076,28 @@ export async function updateSessionToolPolicy(
4043
5076
  return withFirstPartyTools(validatedTools, runtimeSettings);
4044
5077
  })()
4045
5078
  : null;
4046
- const explicitRequestedFirstPartyTools = explicitRequest
4047
- ? [...explicitRequest.firstPartyMcpTools]
4048
- : null;
4049
5079
  const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(deps.settings);
4050
- const disallowedFirstPartyMcpTool = explicitRequestedFirstPartyTools?.find(
4051
- (tool) => !deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
4052
- );
4053
- if (disallowedFirstPartyMcpTool) {
4054
- throw new HTTPException(422, {
4055
- message: `first-party MCP tool is disabled by deployment policy: ${disallowedFirstPartyMcpTool}`,
4056
- });
5080
+ // Echoing the session's stored catalog (or create defaults) can include a
5081
+ // tool the deployment later removed. Runtime already strips those; failing
5082
+ // the whole connector save would block unrelated MCP toggles. A newly added
5083
+ // disabled tool, however, is explicit intent and fails rather than vanishing.
5084
+ if (explicitRequest) {
5085
+ requireDeploymentFirstPartyMcpTools(
5086
+ explicitRequest.firstPartyMcpTools,
5087
+ deploymentFirstPartyMcpToolPolicy.allowed,
5088
+ [
5089
+ // A session without a stored selection was shown the unfiltered defaults.
5090
+ ...(existingSession.firstPartyMcpTools ??
5091
+ deps.settings.defaultFirstPartyMcpTools ??
5092
+ DEFAULT_FIRST_PARTY_MCP_TOOLS),
5093
+ ...(workspaceSessionToolDefaults?.firstPartyMcpTools ?? []),
5094
+ ...(existingSession.toolPolicy.firstPartyAdditions ?? []),
5095
+ ],
5096
+ );
4057
5097
  }
5098
+ const explicitRequestedFirstPartyTools = explicitRequest
5099
+ ? allowedFirstPartyMcpToolsForSession(deps.settings, explicitRequest.firstPartyMcpTools)
5100
+ : null;
4058
5101
  const workspaceDefaultTools = withFirstPartyTools(
4059
5102
  withWorkspaceDefaultMcpTools(
4060
5103
  [],
@@ -4082,6 +5125,11 @@ export async function updateSessionToolPolicy(
4082
5125
  let nextTools: ToolRef[];
4083
5126
  let nextFirstPartyMcpTools: FirstPartyMcpToolName[];
4084
5127
  let nextPolicy: SessionToolPolicy;
5128
+ // Guaranteed additions belong to the session, not to one tool choice: an
5129
+ // explicit edit stops applying them and a reset to defaults restores them.
5130
+ const sessionFirstPartyAdditions = session.toolPolicy.firstPartyAdditions?.length
5131
+ ? { firstPartyAdditions: [...session.toolPolicy.firstPartyAdditions] }
5132
+ : {};
4085
5133
  if (session.parentSessionId) {
4086
5134
  const parent = await context.getLockedSession(session.parentSessionId);
4087
5135
  if (!parent) {
@@ -4090,23 +5138,13 @@ export async function updateSessionToolPolicy(
4090
5138
  });
4091
5139
  }
4092
5140
  const parentTracksWorkspaceDefaults = parent.toolPolicy?.mode === "workspace_default";
4093
- const parentEffective = withFirstPartyTools(
4094
- parentTracksWorkspaceDefaults
4095
- ? withWorkspaceDefaultMcpTools(
4096
- availableToolRefs(parent.tools, runtimeSettings),
4097
- deps.settings,
4098
- runtimeSettings,
4099
- workspaceSessionToolDefaults,
4100
- )
4101
- : parent.tools,
4102
- runtimeSettings,
4103
- );
4104
- const deploymentAllowedFirstPartyMcpTools = new Set(
4105
- deploymentFirstPartyMcpToolPolicy.allowed,
4106
- );
4107
- const parentFirstPartyMcpTools = [
4108
- ...(parent.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default),
4109
- ].filter((tool) => deploymentAllowedFirstPartyMcpTools.has(tool));
5141
+ const { tools: parentEffective, firstPartyMcpTools: parentFirstPartyMcpTools } =
5142
+ sessionToolPolicyCeiling(
5143
+ parent,
5144
+ deps.settings,
5145
+ runtimeSettings,
5146
+ workspaceSessionToolDefaults,
5147
+ );
4110
5148
  if (requestedMode === "workspace_default") {
4111
5149
  if (!parentTracksWorkspaceDefaults) {
4112
5150
  throw new HTTPException(403, {
@@ -4119,6 +5157,18 @@ export async function updateSessionToolPolicy(
4119
5157
  nextPolicy = {
4120
5158
  mode: "workspace_default",
4121
5159
  inheritedFromSessionId: parent.id,
5160
+ ...(parent.toolPolicy.firstPartyMode === "workspace_default"
5161
+ ? {
5162
+ firstPartyMode: "workspace_default" as const,
5163
+ ...(parent.toolPolicy.firstPartyAdditions?.length
5164
+ ? { firstPartyAdditions: [...parent.toolPolicy.firstPartyAdditions] }
5165
+ : {}),
5166
+ }
5167
+ : {}),
5168
+ ...defaultPolicyExclusions([
5169
+ ...(parent.toolPolicy.excludedMcpServerIds ?? []),
5170
+ ...requestedExclusions,
5171
+ ]),
4122
5172
  };
4123
5173
  } else {
4124
5174
  nextTools = explicitRequestedTools!;
@@ -4133,7 +5183,7 @@ export async function updateSessionToolPolicy(
4133
5183
  );
4134
5184
  if (widenedFirstPartyTool) {
4135
5185
  throw new HTTPException(403, {
4136
- message: `session OpenGeni tools may only narrow the parent policy: ${widenedFirstPartyTool}`,
5186
+ message: `session Opengeni tools may only narrow the parent policy: ${widenedFirstPartyTool}`,
4137
5187
  });
4138
5188
  }
4139
5189
  nextFirstPartyMcpTools = explicitRequestedFirstPartyTools!;
@@ -4147,43 +5197,131 @@ export async function updateSessionToolPolicy(
4147
5197
  requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools!;
4148
5198
  nextFirstPartyMcpTools =
4149
5199
  requestedMode === "workspace_default"
4150
- ? workspaceDefaultFirstPartyTools
5200
+ ? [
5201
+ ...new Set([
5202
+ ...workspaceDefaultFirstPartyTools,
5203
+ ...(session.toolPolicy.firstPartyAdditions ?? []).filter((tool) =>
5204
+ deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
5205
+ ),
5206
+ ]),
5207
+ ]
4151
5208
  : explicitRequestedFirstPartyTools!;
4152
- nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
4153
- if (agentAttemptCaller) {
4154
- // A human or API key may widen a top-level session; a live agent
4155
- // attempt may only narrow relative to the session's CURRENT
4156
- // effective policy, in either mode. Adopting workspace defaults is a
4157
- // widen whenever it adds a server or tool the session does not hold.
4158
- const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
4159
- const currentEffectiveTools = withFirstPartyTools(
4160
- sessionTracksWorkspaceDefaults
4161
- ? withWorkspaceDefaultMcpTools(
4162
- availableToolRefs(session.tools, runtimeSettings),
5209
+ nextPolicy = {
5210
+ mode: requestedMode,
5211
+ inheritedFromSessionId: null,
5212
+ ...sessionFirstPartyAdditions,
5213
+ ...(requestedMode === "workspace_default"
5214
+ ? {
5215
+ firstPartyMode: "workspace_default" as const,
5216
+ ...defaultPolicyExclusions(requestedExclusions),
5217
+ }
5218
+ : {}),
5219
+ };
5220
+ }
5221
+ if (connectorOnlyEdit) {
5222
+ if (session.toolPolicy.mode !== "workspace_default") {
5223
+ throw new HTTPException(409, {
5224
+ message: "adopt workspace defaults before editing connector exclusions",
5225
+ });
5226
+ }
5227
+ // A connector switch never rewrites built-in tool choices or selected refs.
5228
+ nextTools = session.tools;
5229
+ nextFirstPartyMcpTools = [
5230
+ ...(session.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default),
5231
+ ];
5232
+ delete nextPolicy.firstPartyMode;
5233
+ if (session.toolPolicy.firstPartyMode) {
5234
+ nextPolicy.firstPartyMode = session.toolPolicy.firstPartyMode;
5235
+ }
5236
+ }
5237
+ if (!connectorOnlyEdit) {
5238
+ nextTools = withoutExcludedMcpServers(nextTools, nextPolicy.excludedMcpServerIds);
5239
+ }
5240
+ if (session.agent) {
5241
+ // The agent configuration is the ceiling of a configured session's
5242
+ // legacy tool columns: a tool-policy edit can narrow within it but
5243
+ // never re-add a tool of a capability the configuration turns off.
5244
+ const clamped = applySessionAgentConfigWriteThrough({
5245
+ config: session.agent,
5246
+ firstPartyMcpTools: nextFirstPartyMcpTools,
5247
+ tools: nextTools,
5248
+ toolPolicy: nextPolicy,
5249
+ productServerIds: [
5250
+ ...session.mcpServers.map((server) => server.id),
5251
+ ...(explicitRequest ? explicitRequest.tools.map((tool) => tool.id) : []),
5252
+ ],
5253
+ });
5254
+ nextFirstPartyMcpTools = clamped.firstPartyMcpTools;
5255
+ nextTools = clamped.tools;
5256
+ nextPolicy = clamped.toolPolicy;
5257
+ }
5258
+ if (agentAttemptCaller && !session.parentSessionId) {
5259
+ if (
5260
+ nextPolicy.firstPartyMode === "workspace_default" &&
5261
+ session.toolPolicy.firstPartyMode !== "workspace_default"
5262
+ ) {
5263
+ throw new HTTPException(403, {
5264
+ message: "an agent may not replace a pinned built-in selection with future defaults",
5265
+ });
5266
+ }
5267
+ // A human or API key may widen a top-level session; a live agent
5268
+ // attempt may only narrow relative to the session's CURRENT
5269
+ // effective policy, in either mode. Adopting workspace defaults is a
5270
+ // widen whenever it adds a server or tool the session does not hold.
5271
+ const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
5272
+ const currentEffectiveTools = withFirstPartyTools(
5273
+ sessionTracksWorkspaceDefaults
5274
+ ? withWorkspaceDefaultMcpTools(
5275
+ availableToolRefs(session.tools, runtimeSettings),
5276
+ deps.settings,
5277
+ runtimeSettings,
5278
+ workspaceSessionToolDefaults,
5279
+ )
5280
+ : session.tools,
5281
+ runtimeSettings,
5282
+ );
5283
+ const currentAllowedTools = withoutExcludedMcpServers(
5284
+ currentEffectiveTools,
5285
+ session.toolPolicy.excludedMcpServerIds,
5286
+ );
5287
+ const nextEffectiveTools =
5288
+ nextPolicy.mode === "workspace_default"
5289
+ ? withFirstPartyTools(
5290
+ withWorkspaceDefaultMcpTools(
5291
+ availableToolRefs(nextTools, runtimeSettings),
4163
5292
  deps.settings,
4164
5293
  runtimeSettings,
4165
5294
  workspaceSessionToolDefaults,
4166
- )
4167
- : session.tools,
4168
- runtimeSettings,
4169
- );
4170
- assertToolRefsSubset(
4171
- nextTools,
4172
- currentEffectiveTools,
4173
- "an agent may only narrow its session tool policy",
4174
- );
4175
- const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
4176
- session.firstPartyMcpTools,
4177
- deploymentFirstPartyMcpToolPolicy,
4178
- );
4179
- const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
4180
- (tool) => !currentFirstPartyCeiling.has(tool),
4181
- );
4182
- if (widenedFirstPartyTool) {
4183
- throw new HTTPException(403, {
4184
- message: `an agent may only narrow its session OpenGeni tools: ${widenedFirstPartyTool}`,
4185
- });
4186
- }
5295
+ ),
5296
+ runtimeSettings,
5297
+ )
5298
+ : nextTools;
5299
+ if (
5300
+ nextPolicy.mode === "workspace_default" &&
5301
+ (session.toolPolicy.excludedMcpServerIds ?? []).some(
5302
+ (id) => !nextPolicy.excludedMcpServerIds?.includes(id),
5303
+ )
5304
+ ) {
5305
+ throw new HTTPException(403, {
5306
+ message: "an agent may not remove session connector exclusions",
5307
+ });
5308
+ }
5309
+ assertToolRefsSubset(
5310
+ withoutExcludedMcpServers(nextEffectiveTools, nextPolicy.excludedMcpServerIds),
5311
+ currentAllowedTools,
5312
+ "an agent may only narrow its session tool policy",
5313
+ );
5314
+ const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
5315
+ resolveSessionFirstPartyMcpTools(deps.settings, session, workspace.settings),
5316
+ deploymentFirstPartyMcpToolPolicy,
5317
+ );
5318
+ const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
5319
+ (tool) => !currentFirstPartyCeiling.has(tool),
5320
+ );
5321
+ if (widenedFirstPartyTool) {
5322
+ throw new HTTPException(403, {
5323
+ message: `an agent may only narrow its session Opengeni tools: ${widenedFirstPartyTool}`,
5324
+ });
4187
5325
  }
4188
5326
  }
4189
5327
 
@@ -4248,6 +5386,353 @@ export async function updateSessionToolPolicy(
4248
5386
  return await requireSession(deps.db, grant.workspaceId, sessionId);
4249
5387
  }
4250
5388
 
5389
+ /**
5390
+ * Replace a running session's agent configuration (`PUT .../agent`). Omitted
5391
+ * request fields keep their current values; a legacy (null) session converts
5392
+ * from its current effective state without widening. The configuration and
5393
+ * its write-through to the legacy tool columns (and the instructions alias)
5394
+ * commit under the same tool-policy version CAS, so tool-policy and agent
5395
+ * updates cannot race. Effective from the next attempt. Agent callers may only
5396
+ * narrow; a child never widens past its parent.
5397
+ */
5398
+ export async function updateSessionAgent(
5399
+ deps: {
5400
+ db: Database;
5401
+ bus: EventBus;
5402
+ settings: Settings;
5403
+ sessionAuthorization?: SessionAuthorizationPort | null;
5404
+ },
5405
+ grant: AccessGrant,
5406
+ sessionId: string,
5407
+ request: UpdateSessionAgentRequest,
5408
+ ): Promise<Session> {
5409
+ await requireSessionAuthorization(deps, grant, {
5410
+ sessionId,
5411
+ operation: "session.tool_policy.write",
5412
+ surface: "core",
5413
+ });
5414
+ requirePermission(grant, "sessions:control");
5415
+ const agentAttemptCaller = grantHasAgentAttemptAuthority(grant);
5416
+ const workspace = await requireWorkspace(deps.db, grant.workspaceId);
5417
+ const goal = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
5418
+ const goalOpen = goal !== null && goal.status !== "completed";
5419
+ const deploymentPolicy = agentConfigDeploymentPolicy(deps.settings);
5420
+ const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(deps.settings);
5421
+ const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
5422
+ const defaultFirstPartyTools = [
5423
+ ...(workspaceSessionToolDefaults?.firstPartyMcpTools?.filter((tool) =>
5424
+ deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
5425
+ ) ?? deploymentFirstPartyMcpToolPolicy.default),
5426
+ ];
5427
+ const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
5428
+ deps.db,
5429
+ grant.workspaceId,
5430
+ deps.settings,
5431
+ {
5432
+ subjectId: grant.subjectId,
5433
+ },
5434
+ );
5435
+ const runtimeMcpServers = capabilityRuntimeSettings.mcpServers;
5436
+ const runtimeServerIds = new Set(runtimeMcpServers.map((server) => server.id));
5437
+ const workspaceDefaultServerIds = workspaceSessionToolPolicyDefaultServerIdsFor(
5438
+ runtimeMcpServers,
5439
+ workspace.settings,
5440
+ );
5441
+ const events = await appendSessionEventsWithLockedSessionUpdate(
5442
+ deps.db,
5443
+ grant.workspaceId,
5444
+ sessionId,
5445
+ async (session, context) => {
5446
+ const currentVersion = session.toolPolicyVersion ?? 1;
5447
+ if (request.expectedVersion !== currentVersion) {
5448
+ throw new SessionToolPolicyVersionConflictError(currentVersion);
5449
+ }
5450
+ let parentCeiling: ResolvedAgentCapabilities | undefined;
5451
+ let parent: Session | null = null;
5452
+ if (session.parentSessionId) {
5453
+ parent = await context.getLockedSession(session.parentSessionId);
5454
+ if (!parent) {
5455
+ throw new HTTPException(409, { message: "parent session is no longer available" });
5456
+ }
5457
+ parentCeiling =
5458
+ parent.agent?.capabilities ??
5459
+ legacySessionAgentCapabilities(deps.settings, parent, workspace.settings);
5460
+ }
5461
+ const parentTools = parent
5462
+ ? sessionToolPolicyCeiling(
5463
+ parent,
5464
+ deps.settings,
5465
+ settingsWithSessionMcpServerMetadata(capabilityRuntimeSettings, session.mcpServers),
5466
+ workspaceSessionToolDefaults,
5467
+ )
5468
+ : null;
5469
+ const legacyCeiling = legacySessionAgentCapabilities(
5470
+ deps.settings,
5471
+ session,
5472
+ workspace.settings,
5473
+ workspaceDefaultServerIds,
5474
+ );
5475
+ const previousCapabilities = session.agent?.capabilities ?? legacyCeiling;
5476
+ const next = withAgentConfigHttpErrors(() =>
5477
+ resolveAgentConfigUpdate({
5478
+ current: session.agent,
5479
+ legacyCeiling,
5480
+ request: request.agent,
5481
+ deployment: deploymentPolicy,
5482
+ onlyNarrow: agentAttemptCaller,
5483
+ ...(parentCeiling ? { parentCeiling } : {}),
5484
+ goal: goalOpen,
5485
+ }),
5486
+ );
5487
+ const currentFirstPartyMcpTools = resolveSessionFirstPartyMcpTools(
5488
+ deps.settings,
5489
+ session,
5490
+ workspace.settings,
5491
+ );
5492
+ const added = agentAttemptCaller
5493
+ ? []
5494
+ : agentConfigAddedFirstPartyMcpTools(
5495
+ previousCapabilities,
5496
+ next,
5497
+ parentTools?.firstPartyMcpTools ?? defaultFirstPartyTools,
5498
+ );
5499
+ let tools = [...session.tools];
5500
+ let toolPolicy: SessionToolPolicy = { ...session.toolPolicy };
5501
+ if (!agentAttemptCaller) {
5502
+ // Re-enabled built-ins come back; re-enabled connectors track the
5503
+ // workspace defaults again for a top-level session.
5504
+ const reEnabled = (id: "workspaceFiles" | "knowledge" | "workspaceConnectors") =>
5505
+ !previousCapabilities[id] && next.capabilities[id];
5506
+ for (const [capability, serverId] of [
5507
+ ["workspaceFiles", "files"],
5508
+ ["knowledge", "docs"],
5509
+ ] as const) {
5510
+ if (!reEnabled(capability)) continue;
5511
+ if (toolPolicy.excludedMcpServerIds?.includes(serverId)) {
5512
+ const excluded = toolPolicy.excludedMcpServerIds.filter((id) => id !== serverId);
5513
+ const { excludedMcpServerIds: _removed, ...rest } = toolPolicy;
5514
+ toolPolicy = excluded.length ? { ...rest, excludedMcpServerIds: excluded } : rest;
5515
+ } else if (
5516
+ toolPolicy.mode !== "workspace_default" &&
5517
+ runtimeServerIds.has(serverId) &&
5518
+ !tools.some((tool) => tool.id === serverId)
5519
+ ) {
5520
+ tools.push({ kind: "mcp", id: serverId, optional: true });
5521
+ }
5522
+ const parentTool = parentTools?.tools.find((tool) => tool.id === serverId);
5523
+ if (parentTool && !tools.some((tool) => tool.id === serverId)) tools.push(parentTool);
5524
+ }
5525
+ if (
5526
+ reEnabled("workspaceConnectors") &&
5527
+ !session.parentSessionId &&
5528
+ toolPolicy.mode === "explicit"
5529
+ ) {
5530
+ toolPolicy = { ...toolPolicy, mode: "workspace_default", inheritedFromSessionId: null };
5531
+ }
5532
+ }
5533
+ let firstPartyMcpTools = [...new Set([...currentFirstPartyMcpTools, ...added])];
5534
+ if (parent && parentTools) {
5535
+ // Family booleans are not exact tool authority: re-enabling a family
5536
+ // restores only selections the locked parent actually permits.
5537
+ const runtimeSettings = settingsWithSessionMcpServerMetadata(
5538
+ capabilityRuntimeSettings,
5539
+ session.mcpServers,
5540
+ );
5541
+ const parentFirstPartyTools = new Set(parentTools.firstPartyMcpTools);
5542
+ firstPartyMcpTools = firstPartyMcpTools.filter((tool) => parentFirstPartyTools.has(tool));
5543
+ if (toolPolicy.mode === "workspace_default") {
5544
+ tools = withoutExcludedMcpServers(
5545
+ withFirstPartyTools(
5546
+ withWorkspaceDefaultMcpTools(
5547
+ availableToolRefs(tools, runtimeSettings),
5548
+ deps.settings,
5549
+ runtimeSettings,
5550
+ workspaceSessionToolDefaults,
5551
+ ),
5552
+ runtimeSettings,
5553
+ ),
5554
+ toolPolicy.excludedMcpServerIds,
5555
+ );
5556
+ if (parent.toolPolicy.mode !== "workspace_default") {
5557
+ // An explicit parent cannot authorize future workspace additions.
5558
+ toolPolicy = { mode: "inherited", inheritedFromSessionId: parent.id };
5559
+ } else {
5560
+ toolPolicy = {
5561
+ ...toolPolicy,
5562
+ ...defaultPolicyExclusions([
5563
+ ...(parent.toolPolicy.excludedMcpServerIds ?? []),
5564
+ ...(toolPolicy.excludedMcpServerIds ?? []),
5565
+ ]),
5566
+ };
5567
+ }
5568
+ }
5569
+ const parentToolIds = new Set(parentTools.tools.map((tool) => `${tool.kind}:${tool.id}`));
5570
+ tools = tools.filter((tool) => parentToolIds.has(`${tool.kind}:${tool.id}`));
5571
+ }
5572
+ const writeThrough = applySessionAgentConfigWriteThrough({
5573
+ config: next,
5574
+ firstPartyMcpTools,
5575
+ tools,
5576
+ toolPolicy,
5577
+ productServerIds: [
5578
+ ...session.mcpServers.map((server) => server.id),
5579
+ ...(session.toolPolicy.mode !== "workspace_default"
5580
+ ? session.tools.map((tool) => tool.id)
5581
+ : []),
5582
+ ],
5583
+ });
5584
+ tools = writeThrough.tools;
5585
+ toolPolicy = writeThrough.toolPolicy;
5586
+ const nextInstructions =
5587
+ request.agent.instructions !== undefined
5588
+ ? request.agent.instructions
5589
+ : (session.instructions ?? null);
5590
+ if (goalOpen && !writeThrough.firstPartyMcpTools.includes("goal_complete")) {
5591
+ throw new HTTPException(422, {
5592
+ message: "a session with an open goal must keep its goal tools",
5593
+ });
5594
+ }
5595
+ const unchanged =
5596
+ stableJson({
5597
+ agent: session.agent,
5598
+ instructions: session.instructions ?? null,
5599
+ firstPartyMcpTools: currentFirstPartyMcpTools,
5600
+ tools: session.tools,
5601
+ policy: session.toolPolicy,
5602
+ }) ===
5603
+ stableJson({
5604
+ agent: next,
5605
+ instructions: nextInstructions,
5606
+ firstPartyMcpTools: writeThrough.firstPartyMcpTools,
5607
+ tools,
5608
+ policy: toolPolicy,
5609
+ });
5610
+ if (unchanged) return { events: [] };
5611
+ const nextVersion = currentVersion + 1;
5612
+ return {
5613
+ events: [
5614
+ {
5615
+ type: "session.agent.updated" as const,
5616
+ payload: {
5617
+ before: session.agent,
5618
+ after: next,
5619
+ instructionsChanged: nextInstructions !== (session.instructions ?? null),
5620
+ toolPolicy: {
5621
+ before: toolPolicyAuditSnapshot(
5622
+ session,
5623
+ session.tools,
5624
+ currentFirstPartyMcpTools,
5625
+ session.toolPolicy,
5626
+ ),
5627
+ after: toolPolicyAuditSnapshot(
5628
+ session,
5629
+ tools,
5630
+ writeThrough.firstPartyMcpTools,
5631
+ toolPolicy,
5632
+ ),
5633
+ },
5634
+ version: nextVersion,
5635
+ effectiveFrom: "next_attempt",
5636
+ },
5637
+ },
5638
+ ],
5639
+ update: {
5640
+ agentConfig: next,
5641
+ ...(nextInstructions !== (session.instructions ?? null)
5642
+ ? { instructions: nextInstructions }
5643
+ : {}),
5644
+ tools,
5645
+ firstPartyMcpTools: writeThrough.firstPartyMcpTools,
5646
+ toolPolicy,
5647
+ toolPolicyVersion: nextVersion,
5648
+ expectedToolPolicyVersion: request.expectedVersion,
5649
+ },
5650
+ };
5651
+ },
5652
+ { activity: "semantic", lockParentSession: true },
5653
+ );
5654
+ if (events.length > 0) {
5655
+ await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
5656
+ }
5657
+ return await requireSession(deps.db, grant.workspaceId, sessionId);
5658
+ }
5659
+
5660
+ /**
5661
+ * Replace the Skills a session carries itself. The worker reads them at the
5662
+ * start of every attempt, so the change applies from the next attempt; work
5663
+ * already running keeps the Skill index it started with. Shares the
5664
+ * tool-policy version (409 when stale) with the other agent-configuration
5665
+ * writes. An agent may only remove Skills, never add or change one.
5666
+ */
5667
+ export async function updateSessionSkills(
5668
+ deps: {
5669
+ db: Database;
5670
+ bus: EventBus;
5671
+ sessionAuthorization?: SessionAuthorizationPort | null;
5672
+ },
5673
+ grant: AccessGrant,
5674
+ sessionId: string,
5675
+ request: UpdateSessionSkillsRequest,
5676
+ ): Promise<Session> {
5677
+ await requireSessionAuthorization(deps, grant, {
5678
+ sessionId,
5679
+ operation: "session.tool_policy.write",
5680
+ surface: "core",
5681
+ });
5682
+ requirePermission(grant, "sessions:control");
5683
+ let skills: SessionSkill[];
5684
+ try {
5685
+ skills = SessionSkills.parse(request.skills);
5686
+ } catch (error) {
5687
+ throw new HTTPException(422, {
5688
+ message: error instanceof Error ? error.message : "invalid session Skill selection",
5689
+ });
5690
+ }
5691
+ const agentAttemptCaller = grantHasAgentAttemptAuthority(grant);
5692
+ const events = await appendSessionEventsWithLockedSessionUpdate(
5693
+ deps.db,
5694
+ grant.workspaceId,
5695
+ sessionId,
5696
+ (session) => {
5697
+ const currentVersion = session.toolPolicyVersion ?? 1;
5698
+ if (request.expectedVersion !== currentVersion) {
5699
+ throw new SessionToolPolicyVersionConflictError(currentVersion);
5700
+ }
5701
+ const current = new Set(session.skills.map((skill) => stableJson(skill)));
5702
+ if (agentAttemptCaller && skills.some((skill) => !current.has(stableJson(skill)))) {
5703
+ throw new HTTPException(403, {
5704
+ message: "An agent can only remove Skills from a session.",
5705
+ });
5706
+ }
5707
+ if (stableJson(session.skills) === stableJson(skills)) return { events: [] };
5708
+ const nextVersion = currentVersion + 1;
5709
+ return {
5710
+ events: [
5711
+ {
5712
+ type: "session.skills.updated" as const,
5713
+ payload: {
5714
+ before: session.skills.map((skill) => skill.name),
5715
+ after: skills.map((skill) => skill.name),
5716
+ version: nextVersion,
5717
+ effectiveFrom: "next_attempt",
5718
+ },
5719
+ },
5720
+ ],
5721
+ update: {
5722
+ skills,
5723
+ toolPolicyVersion: nextVersion,
5724
+ expectedToolPolicyVersion: request.expectedVersion,
5725
+ },
5726
+ };
5727
+ },
5728
+ { activity: "semantic" },
5729
+ );
5730
+ if (events.length > 0) {
5731
+ await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
5732
+ }
5733
+ return await requireSession(deps.db, grant.workspaceId, sessionId);
5734
+ }
5735
+
4251
5736
  export async function readSessionLineage(
4252
5737
  deps: Pick<ApiRouteDeps, "db" | "sessionAuthorization">,
4253
5738
  grant: AccessGrant,
@@ -4272,6 +5757,30 @@ export async function readSessionLineage(
4272
5757
  return lineage;
4273
5758
  }
4274
5759
 
5760
+ /**
5761
+ * A server attached in the same root create request is selected by that
5762
+ * attachment. Neither `tools` (an allow-list over the workspace/deployment
5763
+ * registry) nor the workspace-default expansion can name it, so without this
5764
+ * the attached endpoint was stored but never contacted and the model saw no
5765
+ * tools. This grants nothing beyond the attach itself: the caller already
5766
+ * passed `mcp_servers:attach` for exactly this endpoint, and the server's own
5767
+ * approval policy still governs every call. An explicit ref for the same id
5768
+ * wins unchanged so a caller can still mark it `eager` or `optional`; the
5769
+ * added ref is strict, exactly like listing `{ kind: "mcp", id }` explicitly.
5770
+ * Child creates never reach here: an inherited snapshot keeps the parent's
5771
+ * selection and a child may only narrow it.
5772
+ */
5773
+ export function withSameRequestSessionMcpServerTools(
5774
+ tools: ToolRef[],
5775
+ attachedServers: ReadonlyArray<{ id: string }>,
5776
+ ): ToolRef[] {
5777
+ const selectedIds = new Set(tools.map((tool) => tool.id));
5778
+ const missing = attachedServers
5779
+ .filter((server) => !selectedIds.has(server.id))
5780
+ .map((server) => ({ kind: "mcp" as const, id: server.id }));
5781
+ return missing.length === 0 ? tools : mergeToolRefs(tools, missing);
5782
+ }
5783
+
4275
5784
  function withFirstPartyTools(
4276
5785
  tools: ToolRef[],
4277
5786
  runtimeSettings: { mcpServers: Array<{ id: string }> },
@@ -4282,6 +5791,34 @@ function withFirstPartyTools(
4282
5791
  return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
4283
5792
  }
4284
5793
 
5794
+ /**
5795
+ * The executable MCP tool list a follow-up on an existing session freezes
5796
+ * connector accounts against.
5797
+ *
5798
+ * A `workspace_default` session stores only the tool snapshot taken when it
5799
+ * was created and expands the current workspace defaults at run time, so a
5800
+ * connector enabled after creation never enters the stored column. The
5801
+ * composer projects that same expansion and submits account selections for
5802
+ * it, so freezing against the stored column rejected every such selection as
5803
+ * unmatched and never froze its personal delegation. Resolve through the one
5804
+ * ID-only policy resolver the API projection and the worker use, with the
5805
+ * same omitted-tools default, so all three agree on the executable set.
5806
+ */
5807
+ export function sessionToolsForConnectionAccounts(input: {
5808
+ session: Pick<Session, "tools" | "toolPolicy">;
5809
+ /** The resolved runtime registry, including session-local servers. */
5810
+ runtimeMcpServers: Iterable<{ id: string }>;
5811
+ /** The current omitted-tools default for this workspace. */
5812
+ defaultMcpServerIds: Iterable<string>;
5813
+ }): ToolRef[] {
5814
+ return resolveSessionToolPolicy({
5815
+ toolPolicy: input.session.toolPolicy,
5816
+ sessionTools: input.session.tools,
5817
+ availableMcpServerIds: [...input.runtimeMcpServers].map((server) => server.id),
5818
+ defaultMcpServerIds: input.defaultMcpServerIds,
5819
+ }).toolRefs;
5820
+ }
5821
+
4285
5822
  function hasOwnProperty(value: unknown, key: string): boolean {
4286
5823
  return Boolean(
4287
5824
  value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key),