@opengeni/core 1.0.1 → 2.2.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/access/index.d.ts +37 -0
  2. package/dist/application/new-session-drafts.d.ts +8 -1
  3. package/dist/application/session-commands.d.ts +18 -11
  4. package/dist/application/session-tenancy.d.ts +18 -0
  5. package/dist/application/user-resource-grants.d.ts +20 -0
  6. package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
  7. package/dist/chunk-H7X52RI2.js.map +1 -0
  8. package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
  9. package/dist/chunk-VCZFFEPB.js.map +1 -0
  10. package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
  11. package/dist/chunk-ZYCNLYGH.js.map +1 -0
  12. package/dist/dependencies.d.ts +12 -1
  13. package/dist/domain/automations.d.ts +51 -0
  14. package/dist/domain/company-brain-governed-writes.d.ts +70 -0
  15. package/dist/domain/company-profile-agent-admin.d.ts +24 -0
  16. package/dist/domain/connection-authority.d.ts +97 -0
  17. package/dist/domain/connection-authority.js +169 -0
  18. package/dist/domain/connection-authority.js.map +1 -0
  19. package/dist/domain/editable-artifacts/service.d.ts +1 -2
  20. package/dist/domain/environments.d.ts +1 -1
  21. package/dist/domain/github-repository-bindings.d.ts +117 -0
  22. package/dist/domain/governed-learning-activation.d.ts +16 -0
  23. package/dist/domain/governed-learning-evaluator.d.ts +6 -0
  24. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  25. package/dist/domain/memory-slack-delivery.d.ts +1 -4
  26. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  27. package/dist/domain/packs.d.ts +5 -1
  28. package/dist/domain/personal-connection-delegations.d.ts +34 -2
  29. package/dist/domain/pr-review.d.ts +149 -0
  30. package/dist/domain/remember.d.ts +69 -0
  31. package/dist/domain/resources.d.ts +22 -1
  32. package/dist/domain/scheduled-tasks.d.ts +43 -2
  33. package/dist/domain/session-tool-policy.d.ts +1 -1
  34. package/dist/domain/sessions.d.ts +40 -6
  35. package/dist/editable-artifact-live/application.d.ts +10 -2
  36. package/dist/editable-artifact-live/ports.d.ts +0 -1
  37. package/dist/editable-artifact-live/server.d.ts +0 -1
  38. package/dist/editable-artifact-live/ticket.d.ts +0 -1
  39. package/dist/editable-artifact-live/types.d.ts +5 -2
  40. package/dist/editable-artifact-live.js +2 -2
  41. package/dist/editable-artifacts.js +2 -4
  42. package/dist/index.d.ts +13 -1
  43. package/dist/index.js +4827 -1164
  44. package/dist/index.js.map +1 -1
  45. package/dist/rigs/index.d.ts +22 -5
  46. package/dist/sandbox/fleet.d.ts +34 -6
  47. package/dist/sandbox/routing.d.ts +15 -1
  48. package/dist/sandbox/runtime-settings.d.ts +39 -0
  49. package/package.json +16 -11
  50. package/src/access/index.ts +157 -31
  51. package/src/application/new-session-drafts.ts +24 -2
  52. package/src/application/session-commands.ts +305 -71
  53. package/src/application/session-tenancy.ts +321 -0
  54. package/src/application/user-resource-grants.ts +140 -0
  55. package/src/billing/limits.ts +25 -15
  56. package/src/dependencies.ts +12 -1
  57. package/src/domain/automations.ts +196 -0
  58. package/src/domain/capabilities.ts +7 -1
  59. package/src/domain/company-brain-governed-writes.ts +330 -0
  60. package/src/domain/company-profile-agent-admin.ts +55 -0
  61. package/src/domain/connection-authority.ts +329 -0
  62. package/src/domain/editable-artifacts/agent-application.ts +17 -9
  63. package/src/domain/editable-artifacts/service.ts +12 -1
  64. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  65. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  66. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  67. package/src/domain/environments.ts +16 -3
  68. package/src/domain/github-repository-bindings.ts +295 -0
  69. package/src/domain/governed-learning-activation.ts +44 -0
  70. package/src/domain/governed-learning-evaluator.ts +49 -0
  71. package/src/domain/governed-learning-slack-publication.ts +157 -0
  72. package/src/domain/insights.ts +9 -0
  73. package/src/domain/memory-slack-delivery.ts +0 -30
  74. package/src/domain/organization-membership-lifecycle.ts +9 -0
  75. package/src/domain/packs.ts +106 -3
  76. package/src/domain/personal-connection-delegations.ts +532 -51
  77. package/src/domain/pr-review.ts +671 -0
  78. package/src/domain/remember.ts +549 -0
  79. package/src/domain/resources.ts +131 -4
  80. package/src/domain/scheduled-tasks.ts +497 -76
  81. package/src/domain/session-tool-policy.ts +12 -9
  82. package/src/domain/sessions.ts +613 -166
  83. package/src/editable-artifact-live/application.ts +21 -3
  84. package/src/editable-artifact-live/ports.ts +0 -1
  85. package/src/editable-artifact-live/server.ts +13 -7
  86. package/src/editable-artifact-live/ticket.ts +1 -5
  87. package/src/editable-artifact-live/types.ts +6 -2
  88. package/src/editable-artifact-live/wire.ts +0 -10
  89. package/src/index.ts +13 -1
  90. package/src/rigs/index.ts +165 -48
  91. package/src/sandbox/fleet.ts +376 -30
  92. package/src/sandbox/routing.ts +91 -29
  93. package/src/sandbox/runtime-settings.ts +274 -0
  94. package/src/session-authorization.ts +17 -49
  95. package/dist/chunk-6WKPWE5S.js.map +0 -1
  96. package/dist/chunk-JJU6I5XD.js.map +0 -1
  97. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  98. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  99. package/src/domain/durable-learning-slack-publication.ts +0 -202
@@ -13,24 +13,28 @@ import {
13
13
  CreateSessionRequest,
14
14
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
15
15
  DEFAULT_FIRST_PARTY_MCP_TOOLS,
16
+ DraftTimelineAnnotations,
16
17
  FIRST_PARTY_MCP_TOOL_NAMES,
17
18
  OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
18
19
  SessionSpawnDenial,
19
20
  ServiceTurnInitiator,
20
21
  ServiceTurnInitiatorContext,
21
22
  evaluateWorkspaceModelPolicy,
22
- latencyModeForMetadata,
23
- reasoningEffortForMetadata,
23
+ resolveWorkspaceSessionToolDefaults,
24
24
  stableJson,
25
25
  type AccessGrant,
26
+ type ComposerDraft,
26
27
  type CreateSessionResponse,
27
28
  type GoalSpec,
28
29
  type FirstPartyMcpToolName,
29
30
  type McpPersonalConnectionDelegation,
31
+ type McpConnectionAuthoritySelection,
30
32
  type Permission,
33
+ type PersonalResourceAttachmentIntent,
31
34
  type ReasoningEffort,
32
35
  type ResourceRef,
33
36
  type Session,
37
+ type SessionCommandReceipt,
34
38
  type SessionSkill,
35
39
  type SessionEvent,
36
40
  SessionMcpApprovalPolicy,
@@ -44,6 +48,8 @@ import {
44
48
  type SessionAuthorizationPort,
45
49
  type SessionToolPolicy,
46
50
  type SessionTurn,
51
+ type SessionPromptRouting,
52
+ type SessionGoalSnapshot,
47
53
  type ToolRef,
48
54
  type TurnInitiator,
49
55
  type TurnInitiatorContext,
@@ -63,13 +69,18 @@ import {
63
69
  listDistinctRigVersionIdsInGroup,
64
70
  getSandbox,
65
71
  getSession,
72
+ getSessionAuthorityProjection,
66
73
  SessionIdConflictError,
74
+ NewSessionDraftConflictError,
67
75
  getSessionSpawnDenialByIdempotencyKey,
68
76
  getWorkspaceControlEvent,
69
77
  getSessionLineage,
70
78
  getSessionTurn,
79
+ getSessionTurnForAttempt,
80
+ getSessionTurnPersonalConnectionDelegations,
71
81
  getSessionTurnXaiProviderAccountAuthoritySnapshot,
72
82
  getWorkspaceModelPolicy,
83
+ requireWorkspace,
73
84
  initializeSessionStartAtomically,
74
85
  listSessionTurns,
75
86
  listSessionMcpServersForChildInheritance,
@@ -87,7 +98,13 @@ import {
87
98
  SessionSpawnDeniedDbError,
88
99
  SessionControlConflictError,
89
100
  SessionToolPolicyVersionConflictError,
101
+ SessionCreateIdempotencyConflictError,
102
+ PersonalResourceAttachmentAcceptanceError,
103
+ sessionTenancyProductActivated,
104
+ workspaceControlRequestLockTimeoutMs,
105
+ WorkspaceControlBusyError,
90
106
  type SessionCommandActor,
107
+ type NewSessionDraftSnapshot,
91
108
  } from "@opengeni/db";
92
109
  import {
93
110
  appendAndPublishEvents,
@@ -96,7 +113,7 @@ import {
96
113
  type EventBus,
97
114
  } from "@opengeni/events";
98
115
  import { HTTPException } from "hono/http-exception";
99
- import { hasPermission, requirePermission } from "../access";
116
+ import { hasPermission, requirePermission, type AccessGrantAuthorization } from "../access";
100
117
  import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
101
118
  import type {
102
119
  AcceptSessionUserMessageDependencies,
@@ -104,6 +121,7 @@ import type {
104
121
  SessionWorkflowClient,
105
122
  } from "../dependencies";
106
123
  import {
124
+ requireLiveAgentAttemptAuthorization,
107
125
  requireSessionAuthorization,
108
126
  SessionAuthorizationDeniedError,
109
127
  } from "../session-authorization";
@@ -116,6 +134,10 @@ import {
116
134
  personalConnectionDelegationSourceForGrant,
117
135
  } from "./personal-connection-delegations";
118
136
  import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
137
+ import {
138
+ requireCanonicalManagedHuman,
139
+ requireManagedHumanPrivateSessionCreate,
140
+ } from "../application/session-tenancy";
119
141
  import {
120
142
  assertToolRefsSubset,
121
143
  availableToolRefs,
@@ -124,7 +146,7 @@ import {
124
146
  validateFileResources,
125
147
  validateGitHubRepositorySelection,
126
148
  validateToolRefs,
127
- withDefaultEnabledCapabilityMcpTools,
149
+ withWorkspaceDefaultMcpTools,
128
150
  } from "./resources";
129
151
 
130
152
  const reservedSessionMcpServerIds = new Set(["opengeni", "files", "docs", "codex_apps"]);
@@ -136,6 +158,39 @@ const maxToolPolicyAuditRefs = 40;
136
158
  // RFC 9110 field-name token characters.
137
159
  const sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
138
160
 
161
+ async function requireAtomicPersonalResourceAttachment(
162
+ deps: Pick<ApiRouteDeps, "db">,
163
+ authorization: AccessGrantAuthorization | undefined,
164
+ workspaceId: string,
165
+ intent: PersonalResourceAttachmentIntent | undefined,
166
+ existingSession: boolean,
167
+ ): Promise<void> {
168
+ if (!intent) return;
169
+ if (!authorization) {
170
+ throw new HTTPException(403, {
171
+ message: "Personal resources require the owning managed-human session.",
172
+ });
173
+ }
174
+ try {
175
+ requireCanonicalManagedHuman(authorization, workspaceId);
176
+ } catch (error) {
177
+ throw new HTTPException(403, {
178
+ message: "Personal resources require the owning managed-human session.",
179
+ cause: error,
180
+ });
181
+ }
182
+ if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
183
+ throw new HTTPException(409, {
184
+ message: "Session tenancy is not activated for this organization.",
185
+ });
186
+ }
187
+ if (existingSession && intent.expectedAuthorityEpoch === undefined) {
188
+ throw new HTTPException(422, {
189
+ message: "Personal-resource attachment requires expectedAuthorityEpoch.",
190
+ });
191
+ }
192
+ }
193
+
139
194
  /** Transport-neutral typed denial raised only after its audit row committed. */
140
195
  export class SessionSpawnDeniedError extends Error {
141
196
  readonly denial: SessionSpawnDenial;
@@ -562,6 +617,7 @@ export async function createAndStartSessionWithOutcome(input: {
562
617
  beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
563
618
  accountId: string;
564
619
  workspaceId: string;
620
+ visibility?: "user_private" | "workspace_shared";
565
621
  initialMessage: string;
566
622
  /** Create the session shell without an initial user event/agent turn. */
567
623
  deferInitialTurn?: boolean;
@@ -613,6 +669,7 @@ export async function createAndStartSessionWithOutcome(input: {
613
669
  mcpServers?: CreateSessionMcpServerInput[];
614
670
  sessionMcpServers?: SessionMcpServerMetadata[];
615
671
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
672
+ initialPersonalResourceAttachmentIntent?: PersonalResourceAttachmentIntent | null;
616
673
  xaiProviderAccountAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1;
617
674
  // The manager session spawning this worker (a worker-signed sessionId claim
618
675
  // on the creating grant); null for direct API creates and scheduled runs.
@@ -644,12 +701,27 @@ export async function createAndStartSessionWithOutcome(input: {
644
701
  sandboxId: string;
645
702
  settings: Settings;
646
703
  workingDir?: string | null;
704
+ resourceSubjectId?: string | null;
647
705
  } | null;
648
706
  // Exact actor-private pre-session draft represented by this create. The
649
707
  // initializer consumes it only after the first durable runnable unit commits.
650
708
  consumeNewSessionDraft?: {
651
709
  subjectId: string;
652
710
  expectedRevision: number;
711
+ expectedSnapshot: NewSessionDraftSnapshot;
712
+ acceptedSelection: {
713
+ channelId: string | null;
714
+ targetSandboxId: string | null;
715
+ workingDir: string | null;
716
+ };
717
+ } | null;
718
+ rememberNewSessionSelection?: {
719
+ subjectId: string;
720
+ acceptedSelection: {
721
+ channelId: string | null;
722
+ targetSandboxId: string | null;
723
+ workingDir: string | null;
724
+ };
653
725
  } | null;
654
726
  // A child may lower its inherited nested-agent depth limit freely; increases
655
727
  // are authorized by the caller's workspace:admin grant and checked again by
@@ -673,6 +745,7 @@ export async function createAndStartSessionWithOutcome(input: {
673
745
  ...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
674
746
  accountId: input.accountId,
675
747
  workspaceId: input.workspaceId,
748
+ visibility: input.visibility ?? "workspace_shared",
676
749
  initialMessage: input.initialMessage,
677
750
  initialModelContext: input.modelContext ?? null,
678
751
  resources: input.resources,
@@ -684,6 +757,8 @@ export async function createAndStartSessionWithOutcome(input: {
684
757
  ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
685
758
  createdByActor: input.createdByActor ?? null,
686
759
  model: input.model,
760
+ reasoningEffort: input.reasoningEffort,
761
+ latencyMode: input.latencyMode ?? "standard",
687
762
  sandboxBackend: input.sandboxBackend,
688
763
  variableSetId: input.variableSet?.id ?? null,
689
764
  rigId: input.rigId ?? null,
@@ -699,6 +774,8 @@ export async function createAndStartSessionWithOutcome(input: {
699
774
  ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
700
775
  mcpServers: input.mcpServers ?? [],
701
776
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
777
+ initialPersonalResourceAttachmentIntent:
778
+ input.initialPersonalResourceAttachmentIntent ?? null,
702
779
  ...(input.xaiProviderAccountAuthoritySnapshot
703
780
  ? {
704
781
  initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
@@ -739,6 +816,7 @@ export async function createAndStartSessionWithOutcome(input: {
739
816
  ...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
740
817
  accountId: input.accountId,
741
818
  workspaceId: input.workspaceId,
819
+ visibility: input.visibility ?? "workspace_shared",
742
820
  initialMessage: input.initialMessage,
743
821
  initialModelContext: input.modelContext ?? null,
744
822
  resources: input.resources,
@@ -750,6 +828,8 @@ export async function createAndStartSessionWithOutcome(input: {
750
828
  ...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
751
829
  createdByActor: input.createdByActor ?? null,
752
830
  model: input.model,
831
+ reasoningEffort: input.reasoningEffort,
832
+ latencyMode: input.latencyMode ?? "standard",
753
833
  sandboxBackend: input.sandboxBackend,
754
834
  variableSetId: input.variableSet?.id ?? null,
755
835
  rigId: input.rigId ?? null,
@@ -764,6 +844,8 @@ export async function createAndStartSessionWithOutcome(input: {
764
844
  ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
765
845
  mcpServers: input.mcpServers ?? [],
766
846
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
847
+ initialPersonalResourceAttachmentIntent:
848
+ input.initialPersonalResourceAttachmentIntent ?? null,
767
849
  ...(input.xaiProviderAccountAuthoritySnapshot
768
850
  ? {
769
851
  initialXaiProviderAccountAuthoritySnapshot: input.xaiProviderAccountAuthoritySnapshot,
@@ -825,10 +907,25 @@ async function finishStartSession(
825
907
  sandboxId: string;
826
908
  settings: Settings;
827
909
  workingDir?: string | null;
910
+ resourceSubjectId?: string | null;
828
911
  } | null;
829
912
  consumeNewSessionDraft?: {
830
913
  subjectId: string;
831
914
  expectedRevision: number;
915
+ expectedSnapshot: NewSessionDraftSnapshot;
916
+ acceptedSelection: {
917
+ channelId: string | null;
918
+ targetSandboxId: string | null;
919
+ workingDir: string | null;
920
+ };
921
+ } | null;
922
+ rememberNewSessionSelection?: {
923
+ subjectId: string;
924
+ acceptedSelection: {
925
+ channelId: string | null;
926
+ targetSandboxId: string | null;
927
+ workingDir: string | null;
928
+ };
832
929
  } | null;
833
930
  },
834
931
  session: Session,
@@ -850,6 +947,9 @@ async function finishStartSession(
850
947
  sessionId: session.id,
851
948
  sessionBackend: session.sandboxBackend,
852
949
  sessionGroupId: session.sandboxGroupId,
950
+ ...(input.seedTargetSandbox.resourceSubjectId
951
+ ? { subjectId: input.seedTargetSandbox.resourceSubjectId }
952
+ : {}),
853
953
  };
854
954
  const seeded = await swapActiveSandbox(
855
955
  {
@@ -892,12 +992,19 @@ async function finishStartSession(
892
992
  ...(input.goal.successCriteria !== undefined
893
993
  ? { successCriteria: input.goal.successCriteria }
894
994
  : {}),
995
+ ...(input.goal.rootConstraints !== undefined
996
+ ? { rootConstraints: input.goal.rootConstraints }
997
+ : {}),
895
998
  ...(input.goal.maxAutoContinuations !== undefined
896
999
  ? { maxAutoContinuations: input.goal.maxAutoContinuations }
897
1000
  : {}),
1001
+ ...(input.goal.mutationPolicy !== undefined
1002
+ ? { mutationPolicy: input.goal.mutationPolicy }
1003
+ : {}),
898
1004
  }
899
1005
  : null,
900
1006
  consumeNewSessionDraft: input.consumeNewSessionDraft ?? null,
1007
+ rememberNewSessionSelection: input.rememberNewSessionSelection ?? null,
901
1008
  deferInitialTurn: input.deferInitialTurn === true,
902
1009
  });
903
1010
  await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
@@ -1072,20 +1179,6 @@ export async function requireQueuedTurnForApi(
1072
1179
  return turn;
1073
1180
  }
1074
1181
 
1075
- export function reasoningEffortForSession(
1076
- metadata: Record<string, unknown>,
1077
- fallback: Settings["openaiReasoningEffort"],
1078
- ): Settings["openaiReasoningEffort"] {
1079
- return reasoningEffortForMetadata(metadata, fallback);
1080
- }
1081
-
1082
- export function latencyModeForSession(
1083
- metadata: Record<string, unknown>,
1084
- fallback: "standard" | "priority" | "fast" = "standard",
1085
- ): "standard" | "priority" | "fast" {
1086
- return latencyModeForMetadata(metadata, fallback);
1087
- }
1088
-
1089
1182
  /**
1090
1183
  * Appends a `user.message` to an existing session and enqueues the resulting
1091
1184
  * turn, merging requested resources/tools into the session and waking the
@@ -1111,6 +1204,7 @@ export async function postUserMessageTurn(input: {
1111
1204
  clientEventId?: string;
1112
1205
  mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
1113
1206
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
1207
+ personalResourceAttachment?: PersonalResourceAttachmentIntent;
1114
1208
  delivery?: "send" | "steer";
1115
1209
  origin?: "human" | "operator";
1116
1210
  actor?: string;
@@ -1125,6 +1219,9 @@ export async function postUserMessageTurn(input: {
1125
1219
  }): Promise<{
1126
1220
  accepted: SessionEvent;
1127
1221
  turn: SessionTurn;
1222
+ draft: ComposerDraft | null;
1223
+ receipt: SessionCommandReceipt;
1224
+ routing: SessionPromptRouting;
1128
1225
  interruptionCount: number;
1129
1226
  replay: boolean;
1130
1227
  }> {
@@ -1189,11 +1286,28 @@ export async function postUserMessageTurn(input: {
1189
1286
  ? { recordAgentRunUsage: input.recordAgentRunUsage }
1190
1287
  : {}),
1191
1288
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1289
+ ...(input.personalResourceAttachment
1290
+ ? {
1291
+ personalResourceAttachment: input.personalResourceAttachment,
1292
+ }
1293
+ : {}),
1192
1294
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
1295
+ controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
1193
1296
  }),
1194
1297
  ),
1195
1298
  );
1196
1299
  } catch (error) {
1300
+ if (error instanceof WorkspaceControlBusyError) {
1301
+ // Bounded control-prefix wait expired before any write; the request may
1302
+ // be retried. The API layer renders the retryable 503 envelope.
1303
+ throw error;
1304
+ }
1305
+ if (error instanceof PersonalResourceAttachmentAcceptanceError) {
1306
+ throw new HTTPException(
1307
+ error.kind === "invalid" ? 422 : error.kind === "forbidden" ? 403 : 409,
1308
+ { message: error.message, cause: error },
1309
+ );
1310
+ }
1197
1311
  if (
1198
1312
  error instanceof QueueCommandConflictError ||
1199
1313
  error instanceof SessionControlConflictError
@@ -1209,46 +1323,52 @@ export async function postUserMessageTurn(input: {
1209
1323
  throw error;
1210
1324
  }
1211
1325
  const postCommitTask = async () => {
1212
- try {
1213
- await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1214
- if (result.workspaceControlEventId) {
1215
- const controlEvent = await getWorkspaceControlEvent(
1216
- db,
1217
- workspaceId,
1218
- result.workspaceControlEventId,
1219
- );
1220
- if (!controlEvent) {
1221
- throw new Error(
1222
- `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1223
- );
1326
+ await Promise.all([
1327
+ (async () => {
1328
+ try {
1329
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
1330
+ if (result.workspaceControlEventId) {
1331
+ const controlEvent = await getWorkspaceControlEvent(
1332
+ db,
1333
+ workspaceId,
1334
+ result.workspaceControlEventId,
1335
+ );
1336
+ if (!controlEvent) {
1337
+ throw new Error(
1338
+ `Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
1339
+ );
1340
+ }
1341
+ await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1342
+ }
1343
+ } catch {
1344
+ console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1345
+ errorClass: "PromptEventFanoutOperationError",
1346
+ errorCode: "session_prompt_event_fanout_failed",
1347
+ origin: "core",
1348
+ });
1224
1349
  }
1225
- await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
1226
- }
1227
- } catch {
1228
- console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
1229
- errorClass: "PromptEventFanoutOperationError",
1230
- errorCode: "session_prompt_event_fanout_failed",
1231
- origin: "core",
1232
- });
1233
- }
1234
- try {
1235
- await workflowClient.wakeSessionWorkflow({
1236
- accountId,
1237
- workspaceId,
1238
- sessionId,
1239
- workflowId: result.turn.temporalWorkflowId,
1240
- wakeRevision: result.wakeRevision,
1241
- ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1242
- ? { interruptionRequested: true }
1243
- : {}),
1244
- });
1245
- } catch {
1246
- console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1247
- errorClass: "WorkflowWakeOperationError",
1248
- errorCode: "session_workflow_wake_failed",
1249
- origin: "core",
1250
- });
1251
- }
1350
+ })(),
1351
+ (async () => {
1352
+ try {
1353
+ await workflowClient.wakeSessionWorkflow({
1354
+ accountId,
1355
+ workspaceId,
1356
+ sessionId,
1357
+ workflowId: result.turn.temporalWorkflowId,
1358
+ wakeRevision: result.wakeRevision,
1359
+ ...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
1360
+ ? { interruptionRequested: true }
1361
+ : {}),
1362
+ });
1363
+ } catch {
1364
+ console.warn("[sessions] workflow wake failed; durable outbox will retry", {
1365
+ errorClass: "WorkflowWakeOperationError",
1366
+ errorCode: "session_workflow_wake_failed",
1367
+ origin: "core",
1368
+ });
1369
+ }
1370
+ })(),
1371
+ ]);
1252
1372
  };
1253
1373
  const schedulePostCommit =
1254
1374
  input.schedulePostCommit ??
@@ -1267,6 +1387,33 @@ export async function postUserMessageTurn(input: {
1267
1387
  return {
1268
1388
  accepted: result.accepted,
1269
1389
  turn: result.turn,
1390
+ receipt: {
1391
+ id: result.receipt.id,
1392
+ action: result.receipt.action,
1393
+ operationKey: result.receipt.operationKey,
1394
+ targetSessionId: result.receipt.targetSessionId,
1395
+ targetTurnId: result.receipt.targetTurnId,
1396
+ appliedControlRevision: result.receipt.appliedControlRevision,
1397
+ appliedQueueVersion: result.receipt.appliedQueueVersion,
1398
+ appliedTurnVersion: result.receipt.appliedTurnVersion,
1399
+ appliedDraftRevision: result.receipt.appliedDraftRevision,
1400
+ createdAt: result.receipt.createdAt.toISOString(),
1401
+ },
1402
+ routing: result.routing,
1403
+ draft: result.draft
1404
+ ? {
1405
+ revision: result.draft.revision,
1406
+ text: result.draft.text,
1407
+ annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
1408
+ resources: result.draft.resources as ResourceRef[],
1409
+ model: result.draft.model,
1410
+ reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
1411
+ latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
1412
+ sourceTurnId: result.draft.sourceTurnId,
1413
+ sourceTurnVersion: result.draft.sourceTurnVersion,
1414
+ updatedAt: result.draft.updatedAt.toISOString(),
1415
+ }
1416
+ : null,
1270
1417
  interruptionCount: result.interruptionCount,
1271
1418
  replay: result.replay,
1272
1419
  };
@@ -1281,14 +1428,69 @@ export async function postUserMessageTurn(input: {
1281
1428
  * trusted immediate parent, while explicit arrays (including []) win. A
1282
1429
  * top-level create with omitted tools applies workspace-default capability MCPs.
1283
1430
  */
1431
+ export function resolveChildGoalFromAcceptedSnapshot(
1432
+ goal: GoalSpec,
1433
+ parentGoalSnapshot: SessionGoalSnapshot,
1434
+ ): GoalSpec {
1435
+ const inheritedRootConstraints =
1436
+ parentGoalSnapshot.state === "none" ? [] : parentGoalSnapshot.rootConstraints;
1437
+ const requestedRootConstraints = goal.rootConstraints;
1438
+ if (
1439
+ requestedRootConstraints?.some((constraint) => !inheritedRootConstraints.includes(constraint))
1440
+ ) {
1441
+ throw new Error(
1442
+ "child goal rootConstraints must be an exact subset of the calling turn's frozen root constraints",
1443
+ );
1444
+ }
1445
+ return {
1446
+ ...goal,
1447
+ rootConstraints: requestedRootConstraints ?? inheritedRootConstraints,
1448
+ };
1449
+ }
1450
+
1451
+ export function resolveSessionCreateVisibility(input: {
1452
+ requestedVisibility: "private" | "workspace";
1453
+ visibilityProvided: boolean;
1454
+ parentVisibility: "user_private" | "workspace_shared" | null;
1455
+ }): "user_private" | "workspace_shared" {
1456
+ if (input.parentVisibility === "user_private") {
1457
+ if (input.visibilityProvided && input.requestedVisibility !== "private") {
1458
+ throw new Error("A private parent cannot create a workspace-visible child");
1459
+ }
1460
+ return "user_private";
1461
+ }
1462
+ if (input.parentVisibility === "workspace_shared") {
1463
+ if (input.visibilityProvided && input.requestedVisibility === "private") {
1464
+ throw new Error("A workspace-visible parent cannot create a private child");
1465
+ }
1466
+ return "workspace_shared";
1467
+ }
1468
+ return input.requestedVisibility === "private" ? "user_private" : "workspace_shared";
1469
+ }
1470
+
1284
1471
  export async function createSessionForRequestWithOutcome(
1285
1472
  deps: ApiRouteDeps,
1286
1473
  grant: AccessGrant,
1287
1474
  workspaceId: string,
1288
1475
  rawPayload: unknown,
1476
+ authorization?: AccessGrantAuthorization,
1289
1477
  ): Promise<CreateSessionRequestOutcome> {
1290
1478
  const { settings, db, bus, workflowClient, objectStorage } = deps;
1291
1479
  const payload = CreateSessionRequest.parse(rawPayload);
1480
+ const visibilityProvided = hasOwnProperty(rawPayload, "visibility");
1481
+ if (payload.visibility === "private" && !grant.metadata?.["sessionId"]) {
1482
+ if (!authorization) {
1483
+ throw new HTTPException(403, {
1484
+ message: "managed human session required",
1485
+ });
1486
+ }
1487
+ await requireManagedHumanPrivateSessionCreate(deps, authorization, workspaceId);
1488
+ if (payload.sandbox === "shared" || typeof payload.sandbox === "object") {
1489
+ throw new HTTPException(422, {
1490
+ message: "Only-me sessions require their own sandbox",
1491
+ });
1492
+ }
1493
+ }
1292
1494
  if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
1293
1495
  throw new HTTPException(422, {
1294
1496
  message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
@@ -1308,6 +1510,13 @@ export async function createSessionForRequestWithOutcome(
1308
1510
  throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
1309
1511
  }
1310
1512
  }
1513
+ await requireAtomicPersonalResourceAttachment(
1514
+ deps,
1515
+ authorization,
1516
+ workspaceId,
1517
+ payload.personalResourceAttachment,
1518
+ false,
1519
+ );
1311
1520
  // Parent linkage and execution-context inheritance come ONLY from the
1312
1521
  // worker-signed sessionId claim. A caller cannot nominate a parent in the
1313
1522
  // payload, so inheriting an existing repository/tool/credential snapshot does
@@ -1336,10 +1545,37 @@ export async function createSessionForRequestWithOutcome(
1336
1545
  message: `parent session not found in workspace: ${parentSessionId}`,
1337
1546
  });
1338
1547
  }
1548
+ const workspace = await requireWorkspace(db, workspaceId);
1549
+ const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
1550
+ const parentAuthority = parentSession
1551
+ ? await getSessionAuthorityProjection(db, workspaceId, parentSession.id)
1552
+ : null;
1553
+ if (parentSession && !parentAuthority) {
1554
+ throw new HTTPException(403, {
1555
+ message: "parent session authority is unavailable",
1556
+ });
1557
+ }
1558
+ let effectiveVisibility: "user_private" | "workspace_shared";
1559
+ try {
1560
+ effectiveVisibility = resolveSessionCreateVisibility({
1561
+ requestedVisibility: payload.visibility,
1562
+ visibilityProvided,
1563
+ parentVisibility: parentAuthority?.visibility ?? null,
1564
+ });
1565
+ } catch (error) {
1566
+ throw new HTTPException(422, {
1567
+ message: error instanceof Error ? error.message : "invalid child visibility",
1568
+ });
1569
+ }
1339
1570
  const creationInitiator = creationInitiatorForGrant(grant);
1340
1571
  const parentCallingTurn =
1341
1572
  parentSession && creationInitiator.actor
1342
- ? await getSessionTurn(db, workspaceId, creationInitiator.actor.turnId)
1573
+ ? await getSessionTurnForAttempt(
1574
+ db,
1575
+ workspaceId,
1576
+ parentSession.id,
1577
+ creationInitiator.actor.attemptId,
1578
+ )
1343
1579
  : null;
1344
1580
  if (
1345
1581
  creationInitiator.actor &&
@@ -1349,6 +1585,26 @@ export async function createSessionForRequestWithOutcome(
1349
1585
  message: "caller attempt does not belong to the parent session",
1350
1586
  });
1351
1587
  }
1588
+ let effectiveGoal = payload.goal;
1589
+ if (parentSession && payload.goal) {
1590
+ try {
1591
+ effectiveGoal = resolveChildGoalFromAcceptedSnapshot(
1592
+ payload.goal,
1593
+ parentCallingTurn?.goalSnapshot ?? {
1594
+ state: "none",
1595
+ capturedAt: "unavailable",
1596
+ },
1597
+ );
1598
+ } catch (error) {
1599
+ throw new HTTPException(422, {
1600
+ message: error instanceof Error ? error.message : "invalid child goal root constraints",
1601
+ });
1602
+ }
1603
+ }
1604
+ const personalResourceSubjectId = creationInitiator.actor
1605
+ ? (await requireLiveAgentAttemptAuthorization(db, grant, creationInitiator.actor.sessionId))
1606
+ .initiatingHumanSubjectId
1607
+ : grant.subjectId;
1352
1608
  const xaiProviderAccountAuthoritySnapshot =
1353
1609
  parentSession && creationInitiator.actor
1354
1610
  ? await getSessionTurnXaiProviderAccountAuthoritySnapshot(
@@ -1358,13 +1614,25 @@ export async function createSessionForRequestWithOutcome(
1358
1614
  creationInitiator.actor.turnId,
1359
1615
  )
1360
1616
  : undefined;
1617
+ const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
1618
+ const inheritedPersonalConnectionDelegations =
1619
+ connectionDelegationSource.kind === "turn"
1620
+ ? await getSessionTurnPersonalConnectionDelegations(
1621
+ db,
1622
+ workspaceId,
1623
+ connectionDelegationSource.sessionId,
1624
+ connectionDelegationSource.turnId,
1625
+ )
1626
+ : null;
1361
1627
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
1362
1628
  db,
1363
1629
  workspaceId,
1364
1630
  settings,
1365
- {
1366
- subjectId: grant.subjectId,
1367
- },
1631
+ inheritedPersonalConnectionDelegations
1632
+ ? {
1633
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
1634
+ }
1635
+ : { subjectId: grant.subjectId },
1368
1636
  );
1369
1637
  const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers")
1370
1638
  ? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers)
@@ -1386,6 +1654,10 @@ export async function createSessionForRequestWithOutcome(
1386
1654
  ? payload.skills
1387
1655
  : (parentSession?.skills ?? payload.skills);
1388
1656
  const toolsProvided = hasOwnProperty(rawPayload, "tools");
1657
+ // Visibility became durable draft state after older clients had already
1658
+ // written rows without it. Compare it only when the create request supplied
1659
+ // the field explicitly; the parsed schema default must not manufacture a
1660
+ // mismatch for a legacy draft.
1389
1661
  const requestedTools = validateToolRefs(
1390
1662
  toolsProvided ? payload.tools : (parentSession?.tools ?? payload.tools),
1391
1663
  runtimeSettings,
@@ -1396,10 +1668,11 @@ export async function createSessionForRequestWithOutcome(
1396
1668
  const parentTracksWorkspaceDefaults = parentSession.toolPolicy?.mode === "workspace_default";
1397
1669
  const parentEffective = withFirstPartyTools(
1398
1670
  parentTracksWorkspaceDefaults
1399
- ? withDefaultEnabledCapabilityMcpTools(
1671
+ ? withWorkspaceDefaultMcpTools(
1400
1672
  availableToolRefs(parentSession.tools, runtimeSettings),
1401
1673
  settings,
1402
1674
  runtimeSettings,
1675
+ workspaceSessionToolDefaults,
1403
1676
  )
1404
1677
  : parentSession.tools,
1405
1678
  runtimeSettings,
@@ -1426,10 +1699,11 @@ export async function createSessionForRequestWithOutcome(
1426
1699
  selectedTools = requestedTools;
1427
1700
  toolPolicy = { mode: "explicit", inheritedFromSessionId: null };
1428
1701
  } else {
1429
- selectedTools = withDefaultEnabledCapabilityMcpTools(
1702
+ selectedTools = withWorkspaceDefaultMcpTools(
1430
1703
  requestedTools,
1431
1704
  settings,
1432
1705
  capabilityRuntimeSettings,
1706
+ workspaceSessionToolDefaults,
1433
1707
  );
1434
1708
  toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
1435
1709
  }
@@ -1438,13 +1712,6 @@ export async function createSessionForRequestWithOutcome(
1438
1712
  // tool's permission/target authorization predicate, so attachment alone
1439
1713
  // exposes nothing.
1440
1714
  const tools = withFirstPartyTools(selectedTools, runtimeSettings);
1441
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
1442
- db,
1443
- workspaceId,
1444
- settings: runtimeSettings,
1445
- tools,
1446
- source: personalConnectionDelegationSourceForGrant(grant),
1447
- });
1448
1715
  await validateGitHubRepositorySelection(db, workspaceId, resources);
1449
1716
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
1450
1717
  throw new HTTPException(503, {
@@ -1479,7 +1746,7 @@ export async function createSessionForRequestWithOutcome(
1479
1746
  let frozenRigId: string | null = null;
1480
1747
  let frozenRigVersionId: string | null = null;
1481
1748
  if (requestedRigId) {
1482
- const rig = await getRig(db, workspaceId, requestedRigId);
1749
+ const rig = await getRig(db, grant, requestedRigId);
1483
1750
  if (!rig || !rig.activeVersion) {
1484
1751
  if (payload.rigId) {
1485
1752
  throw new HTTPException(422, {
@@ -1490,6 +1757,14 @@ export async function createSessionForRequestWithOutcome(
1490
1757
  }
1491
1758
  // else: workspace-default fallback that no longer resolves → rig-less.
1492
1759
  } else {
1760
+ for (const defaultVariableSetId of new Set(rig.activeVersion.defaultVariableSetIds)) {
1761
+ await validateVariableSetAttachment(
1762
+ { settings, db },
1763
+ grant,
1764
+ workspaceId,
1765
+ defaultVariableSetId,
1766
+ );
1767
+ }
1493
1768
  frozenRigId = rig.id;
1494
1769
  frozenRigVersionId = rig.activeVersion.id;
1495
1770
  }
@@ -1526,14 +1801,44 @@ export async function createSessionForRequestWithOutcome(
1526
1801
  await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
1527
1802
  const inheritedReasoningEffort =
1528
1803
  parentCallingTurn?.reasoningEffort ??
1529
- (parentSession
1530
- ? reasoningEffortForSession(parentSession.metadata, settings.openaiReasoningEffort)
1531
- : settings.openaiReasoningEffort);
1804
+ parentSession?.reasoningEffort ??
1805
+ settings.openaiReasoningEffort;
1532
1806
  const inheritedLatencyMode =
1533
- parentCallingTurn?.latencyMode ??
1534
- (parentSession ? latencyModeForSession(parentSession.metadata, "standard") : "standard");
1807
+ parentCallingTurn?.latencyMode ?? parentSession?.latencyMode ?? "standard";
1535
1808
  const reasoningEffort = payload.reasoningEffort ?? inheritedReasoningEffort;
1536
1809
  const latencyMode = payload.latencyMode ?? inheritedLatencyMode;
1810
+ if (payload.expectedNewSessionDraftRevision !== undefined && payload.rigId === null) {
1811
+ throw new HTTPException(409, {
1812
+ message: "The submitted session options are not represented by the new-session draft",
1813
+ });
1814
+ }
1815
+ const expectedNewSessionDraftSnapshot: NewSessionDraftSnapshot | null =
1816
+ payload.expectedNewSessionDraftRevision !== undefined
1817
+ ? {
1818
+ text: payload.initialMessage ?? "",
1819
+ resources,
1820
+ tools: toolsProvided ? requestedTools : [],
1821
+ toolsProvided,
1822
+ model,
1823
+ reasoningEffort,
1824
+ latencyMode,
1825
+ options: {
1826
+ ...(visibilityProvided ? { visibility: payload.visibility } : {}),
1827
+ ...(payload.sandboxBackend ? { sandboxBackend: payload.sandboxBackend } : {}),
1828
+ ...(payload.targetSandboxId ? { targetSandboxId: payload.targetSandboxId } : {}),
1829
+ ...(payload.workingDir ? { workingDir: payload.workingDir } : {}),
1830
+ ...(payload.variableSetId ? { variableSetId: payload.variableSetId } : {}),
1831
+ ...(payload.rigId ? { rigId: payload.rigId } : {}),
1832
+ ...(payload.goal ? { goal: payload.goal } : {}),
1833
+ ...(payload.firstPartyMcpPermissions
1834
+ ? { firstPartyMcpPermissions: payload.firstPartyMcpPermissions }
1835
+ : {}),
1836
+ ...(payload.firstPartyMcpTools
1837
+ ? { firstPartyMcpTools: payload.firstPartyMcpTools }
1838
+ : {}),
1839
+ },
1840
+ }
1841
+ : null;
1537
1842
  const inheritedFromParent = parentSession !== null;
1538
1843
  const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
1539
1844
  modelId: model,
@@ -1615,7 +1920,7 @@ export async function createSessionForRequestWithOutcome(
1615
1920
  // and never gains an unrequested permission. Top-level omission remains the
1616
1921
  // deployment's worker default, which includes the goal tools.
1617
1922
  if (
1618
- payload.goal &&
1923
+ effectiveGoal &&
1619
1924
  firstPartyMcpPermissions &&
1620
1925
  !firstPartyMcpPermissions.includes("goals:manage")
1621
1926
  ) {
@@ -1626,7 +1931,8 @@ export async function createSessionForRequestWithOutcome(
1626
1931
  }
1627
1932
  // Tool visibility is independent from permission authority. A child that
1628
1933
  // omits the field inherits the parent's exact effective selection; a
1629
- // top-level omission selects the safe non-connector default catalog.
1934
+ // top-level omission selects the workspace's exact default catalog (or the
1935
+ // deployment default when the workspace has not configured one).
1630
1936
  const deploymentFirstPartyMcpToolPolicy = resolveFirstPartyMcpToolPolicy(settings);
1631
1937
  const disallowedFirstPartyMcpTool = payload.firstPartyMcpTools?.find(
1632
1938
  (tool) => !deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
@@ -1636,12 +1942,37 @@ export async function createSessionForRequestWithOutcome(
1636
1942
  message: `first-party MCP tool is disabled by deployment policy: ${disallowedFirstPartyMcpTool}`,
1637
1943
  });
1638
1944
  }
1945
+ const workspaceFirstPartyDefaults = workspaceSessionToolDefaults?.firstPartyMcpTools.filter(
1946
+ (tool) => deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
1947
+ );
1639
1948
  const firstPartyMcpTools = resolveFirstPartyMcpToolsForCreate(
1640
1949
  payload.firstPartyMcpTools,
1641
1950
  parentSession ? parentSession.firstPartyMcpTools : undefined,
1642
- deploymentFirstPartyMcpToolPolicy,
1951
+ workspaceFirstPartyDefaults && !parentSession
1952
+ ? { ...deploymentFirstPartyMcpToolPolicy, default: workspaceFirstPartyDefaults }
1953
+ : deploymentFirstPartyMcpToolPolicy,
1643
1954
  );
1644
- if (payload.goal) {
1955
+ const googleDrivePublicationEnabled =
1956
+ firstPartyMcpTools.includes("editable_artifact_export") &&
1957
+ firstPartyMcpTools.includes("editable_artifact_export_status") &&
1958
+ (!firstPartyMcpPermissions?.length ||
1959
+ (firstPartyMcpPermissions.includes("artifacts:read") &&
1960
+ firstPartyMcpPermissions.includes("artifacts:publish")));
1961
+ const atlassianEnabled =
1962
+ firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
1963
+ (!firstPartyMcpPermissions?.length || firstPartyMcpPermissions.includes("connections:read"));
1964
+ const personalConnectionDelegations = await freezePersonalConnectionDelegations({
1965
+ db,
1966
+ workspaceId,
1967
+ settings: runtimeSettings,
1968
+ tools,
1969
+ resources,
1970
+ source: connectionDelegationSource,
1971
+ authoritySelections: payload.connectionAuthorities,
1972
+ googleDrivePublicationEnabled,
1973
+ atlassianEnabled,
1974
+ });
1975
+ if (effectiveGoal) {
1645
1976
  const missingGoalTools = ["goal_update", "goal_progress", "goal_complete", "goal_pause"].filter(
1646
1977
  (name) => !firstPartyMcpTools.includes(name as FirstPartyMcpToolName),
1647
1978
  );
@@ -1669,7 +2000,12 @@ export async function createSessionForRequestWithOutcome(
1669
2000
  // spawned FROM INSIDE a session (parentSessionId present ⇒ a worker-signed
1670
2001
  // sessionId claim) defaults to "shared" (join the creator's box); a top-level
1671
2002
  // create (no parent) defaults to "new" (a private singleton box). Explicit
1672
- // values always win.
2003
+ // values always win — except a named `targetSandboxId` is a different compute
2004
+ // home, not a share of the creator's box. Omission plus a machine target
2005
+ // therefore defaults to "new" so the honest-label selfhosted home can fire
2006
+ // (a backend:"none" parent has no box to share; inheriting "none" then 422s
2007
+ // at seed). Explicit shared/{groupId} plus a machine target is contradictory
2008
+ // and 422s rather than silently dropping the target.
1673
2009
  //
1674
2010
  // null sandboxGroupId ⇒ createSession seeds the new row's own id (singleton,
1675
2011
  // today's 1:1 behavior). A shared/{groupId} spawn inherits the box's backend
@@ -1678,7 +2014,14 @@ export async function createSessionForRequestWithOutcome(
1678
2014
  // getAnySessionInGroup are RLS-workspace-scoped, so a foreign parent/group
1679
2015
  // returns null → 404; the group uuid is NOT an access boundary, the workspace
1680
2016
  // filter is (stress (e)).
1681
- const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
2017
+ if (payload.targetSandboxId && payload.sandbox !== undefined && payload.sandbox !== "new") {
2018
+ throw new HTTPException(422, {
2019
+ message:
2020
+ "targetSandboxId requires an own sandbox (omit sandbox or pass 'new'); it cannot join a shared group",
2021
+ });
2022
+ }
2023
+ const sandboxChoice =
2024
+ payload.sandbox ?? (payload.targetSandboxId ? "new" : parentSessionId ? "shared" : "new");
1682
2025
  let sandboxGroupId: string | null = null;
1683
2026
  let inheritedBackend: Session["sandboxBackend"] | undefined;
1684
2027
  // ENV-AWARE GROUPING: under the CURRENT mechanics the workspace VariableSet is
@@ -1816,19 +2159,22 @@ export async function createSessionForRequestWithOutcome(
1816
2159
  "workingDir requires targetSandboxId (it is the targeted machine's working directory)",
1817
2160
  });
1818
2161
  }
1819
- // Honest-label (Stage-D closure): a top-level session TARGETED at a Connected
2162
+ // Honest-label (Stage-D closure): a session TARGETED at a Connected
1820
2163
  // Machine (a selfhosted sandbox) runs machine-primary every turn, so its HOME
1821
2164
  // sandbox_backend must read "selfhosted" — not the deployment cloud default —
1822
2165
  // so the session row + first turn honestly reflect where the agent runs (the
1823
2166
  // Machines dashboard, the turn's warm-metering, and the file-download plane all
1824
- // key off this). GUARDS: (1) only at a TOP-LEVEL create (inheritedBackend
1825
- // undefined) a shared/{groupId} spawn is literally the creator's box and must
1826
- // NOT be relabeled; (2) only when the target's kind is actually "selfhosted" —
1827
- // targetSandboxId also accepts a first-class MODAL sandbox id (resolveTarget),
1828
- // which must never be mislabeled. A not-found / non-selfhosted / modal target
1829
- // falls through to the default; the seed swap in createAndStartSession still
1830
- // validates ownership/liveness and 422s a bad target. (3) only when the feature
1831
- // flags that make the worker actually take the machine-primary path are ON
2167
+ // key off this). GUARDS: (1) only when not inheriting a shared box
2168
+ // (inheritedBackend undefined). Named targetSandboxId already 422s
2169
+ // shared/{groupId} and defaults omission to own-box above, so this check is a
2170
+ // backstop if those placement rules change; a shared spawn without a target
2171
+ // is still literally the creator's box and must NOT be relabeled; (2) only
2172
+ // when the target's kind is actually "selfhosted" targetSandboxId also
2173
+ // accepts a first-class MODAL sandbox id (resolveTarget), which must never be
2174
+ // mislabeled. A not-found / non-selfhosted / modal target falls through to the
2175
+ // default; the seed swap in createAndStartSession still validates
2176
+ // ownership/liveness and 422s a bad target. (3) only when the feature flags
2177
+ // that make the worker actually take the machine-primary path are ON
1832
2178
  // (sandboxOwnershipEnabled + sandboxSelfhostedEnabled/routing) — otherwise the
1833
2179
  // worker ignores the active pointer and a home="selfhosted" turn would fall to
1834
2180
  // the registry client with no bound agentId and throw; with the flags off we
@@ -1849,11 +2195,19 @@ export async function createSessionForRequestWithOutcome(
1849
2195
  settings.sandboxOwnershipEnabled &&
1850
2196
  settings.sandboxSelfhostedEnabled
1851
2197
  ) {
1852
- const targetSandbox = await getSandbox(db, workspaceId, payload.targetSandboxId);
2198
+ const targetSandbox = await getSandbox(
2199
+ db,
2200
+ personalResourceSubjectId ? { ...grant, subjectId: personalResourceSubjectId } : grant,
2201
+ payload.targetSandboxId,
2202
+ );
1853
2203
  if (targetSandbox?.kind === "selfhosted") {
1854
2204
  machineHomeBackend = "selfhosted";
1855
2205
  if (targetSandbox.enrollmentId) {
1856
- const enrollment = await getEnrollment(db, workspaceId, targetSandbox.enrollmentId);
2206
+ const enrollment = await getEnrollment(
2207
+ db,
2208
+ targetSandbox.workspaceId,
2209
+ targetSandbox.enrollmentId,
2210
+ );
1857
2211
  if (
1858
2212
  enrollment &&
1859
2213
  (enrollment.os === "macos" || enrollment.os === "windows" || enrollment.os === "linux")
@@ -1881,6 +2235,7 @@ export async function createSessionForRequestWithOutcome(
1881
2235
  workflowClient,
1882
2236
  accountId: grant.accountId,
1883
2237
  workspaceId,
2238
+ visibility: effectiveVisibility,
1884
2239
  initialMessage: payload.initialMessage ?? "",
1885
2240
  deferInitialTurn: payload.startMode === "realtime",
1886
2241
  modelContext: payload.modelContext ?? null,
@@ -1895,14 +2250,13 @@ export async function createSessionForRequestWithOutcome(
1895
2250
  turnExecutionPolicy,
1896
2251
  // A shared spawn inherits the box's backend; a caller-supplied
1897
2252
  // sandboxBackend on a shared spawn is ignored (it is the same box). A
1898
- // machine-targeted top-level create labels the home "selfhosted"
1899
- // (machineHomeBackend), overriding the caller/deployment default so the row
1900
- // matches where the session actually runs.
2253
+ // machine-targeted create (top-level or own-box child) labels the home
2254
+ // "selfhosted" (machineHomeBackend), overriding the caller/deployment
2255
+ // default so the row matches where the session actually runs.
1901
2256
  sandboxBackend:
1902
2257
  inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
1903
- // Mirror the backend relabel on the OS axis: only a machine-targeted
1904
- // top-level create carries a derived OS; everything else is omitted and the
1905
- // "linux" default holds (shared spawns keep the parent-box behavior).
2258
+ // Mirror the backend relabel on the OS axis: a machine-targeted own-box
2259
+ // create carries a derived OS; shared spawns keep the parent-box behavior.
1906
2260
  ...(machineHomeOs ? { sandboxOs: machineHomeOs } : {}),
1907
2261
  sandboxGroupId,
1908
2262
  metadata: payload.metadata,
@@ -1914,7 +2268,7 @@ export async function createSessionForRequestWithOutcome(
1914
2268
  rigId: frozenRigId,
1915
2269
  rigVersionId: frozenRigVersionId,
1916
2270
  channelId,
1917
- goal: payload.goal ?? null,
2271
+ goal: effectiveGoal ?? null,
1918
2272
  // Per-session persona instructions (already trimmed/validated by the
1919
2273
  // contracts schema). Persisted on the row; composed system-level at turn
1920
2274
  // time. Not surfaced as an event.
@@ -1925,6 +2279,7 @@ export async function createSessionForRequestWithOutcome(
1925
2279
  mcpServers: sessionMcpServers.dbServers,
1926
2280
  sessionMcpServers: sessionMcpServers.metadata,
1927
2281
  personalConnectionDelegations,
2282
+ initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
1928
2283
  ...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
1929
2284
  parentSessionId,
1930
2285
  createIdempotencyKey: payload.idempotencyKey ?? null,
@@ -1940,17 +2295,41 @@ export async function createSessionForRequestWithOutcome(
1940
2295
  sandboxId: payload.targetSandboxId,
1941
2296
  settings,
1942
2297
  workingDir: payload.workingDir ?? null,
2298
+ resourceSubjectId: personalResourceSubjectId,
1943
2299
  }
1944
2300
  : null,
1945
2301
  consumeNewSessionDraft:
1946
- payload.expectedNewSessionDraftRevision !== undefined
2302
+ payload.expectedNewSessionDraftRevision !== undefined && payload.startMode !== "realtime"
1947
2303
  ? {
1948
2304
  subjectId: grant.subjectId,
1949
2305
  expectedRevision: payload.expectedNewSessionDraftRevision,
2306
+ expectedSnapshot: expectedNewSessionDraftSnapshot!,
2307
+ acceptedSelection: {
2308
+ channelId,
2309
+ targetSandboxId: payload.targetSandboxId ?? null,
2310
+ workingDir: payload.targetSandboxId ? (payload.workingDir ?? null) : null,
2311
+ },
2312
+ }
2313
+ : null,
2314
+ rememberNewSessionSelection:
2315
+ payload.expectedNewSessionDraftRevision !== undefined && payload.startMode === "realtime"
2316
+ ? {
2317
+ subjectId: grant.subjectId,
2318
+ acceptedSelection: {
2319
+ channelId,
2320
+ targetSandboxId: payload.targetSandboxId ?? null,
2321
+ workingDir: payload.targetSandboxId ? (payload.workingDir ?? null) : null,
2322
+ },
1950
2323
  }
1951
2324
  : null,
1952
2325
  });
1953
2326
  } catch (error) {
2327
+ if (error instanceof PersonalResourceAttachmentAcceptanceError) {
2328
+ throw new HTTPException(
2329
+ error.kind === "invalid" ? 422 : error.kind === "forbidden" ? 403 : 409,
2330
+ { message: error.message, cause: error },
2331
+ );
2332
+ }
1954
2333
  if (error instanceof AgentCommandAuthorityError) {
1955
2334
  throw new HTTPException(403, { message: error.message });
1956
2335
  }
@@ -1959,6 +2338,15 @@ export async function createSessionForRequestWithOutcome(
1959
2338
  message: "requested session id is already in use",
1960
2339
  });
1961
2340
  }
2341
+ if (error instanceof NewSessionDraftConflictError) {
2342
+ throw new HTTPException(409, {
2343
+ message: error.message,
2344
+ cause: error,
2345
+ });
2346
+ }
2347
+ if (error instanceof SessionCreateIdempotencyConflictError) {
2348
+ throw new HTTPException(409, { message: error.message, cause: error });
2349
+ }
1962
2350
  throw error;
1963
2351
  }
1964
2352
  let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
@@ -2005,8 +2393,11 @@ export async function createSessionForRequest(
2005
2393
  grant: AccessGrant,
2006
2394
  workspaceId: string,
2007
2395
  rawPayload: unknown,
2396
+ authorization?: AccessGrantAuthorization,
2008
2397
  ): Promise<CreateSessionResponse> {
2009
- return (await createSessionForRequestWithOutcome(deps, grant, workspaceId, rawPayload)).session;
2398
+ return (
2399
+ await createSessionForRequestWithOutcome(deps, grant, workspaceId, rawPayload, authorization)
2400
+ ).session;
2010
2401
  }
2011
2402
 
2012
2403
  /**
@@ -2031,14 +2422,20 @@ export async function acceptSessionUserMessageWithOutcome(
2031
2422
  latencyMode?: "standard" | "priority" | "fast" | null;
2032
2423
  clientEventId?: string;
2033
2424
  mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
2425
+ connectionAuthorities?: McpConnectionAuthoritySelection[];
2034
2426
  delivery?: "send" | "steer";
2035
2427
  origin?: "human" | "operator";
2036
2428
  controlEtag?: string | null;
2037
2429
  expectedDraftRevision?: number | null;
2430
+ personalResourceAttachment?: PersonalResourceAttachmentIntent;
2431
+ authorization?: AccessGrantAuthorization;
2038
2432
  },
2039
2433
  ): Promise<{
2040
2434
  accepted: SessionEvent;
2041
2435
  turn: SessionTurn;
2436
+ draft: ComposerDraft | null;
2437
+ receipt: SessionCommandReceipt;
2438
+ routing: SessionPromptRouting;
2042
2439
  interruptionCount: number;
2043
2440
  replay: boolean;
2044
2441
  }> {
@@ -2049,6 +2446,13 @@ export async function acceptSessionUserMessageWithOutcome(
2049
2446
  operation: input.delivery === "steer" ? "session.steer" : "session.append",
2050
2447
  surface: "core",
2051
2448
  });
2449
+ await requireAtomicPersonalResourceAttachment(
2450
+ deps,
2451
+ input.authorization,
2452
+ workspaceId,
2453
+ input.personalResourceAttachment,
2454
+ true,
2455
+ );
2052
2456
  // Hoisted above requireLimit so the codex-billed predicate can resolve the
2053
2457
  // turn's effective model (a follow-up turn inherits the session's model). A
2054
2458
  // pure read with no side effects.
@@ -2067,12 +2471,9 @@ export async function acceptSessionUserMessageWithOutcome(
2067
2471
  }
2068
2472
  throw error;
2069
2473
  }
2070
- const sessionReasoningEffort = reasoningEffortForSession(
2071
- existingSession.metadata,
2072
- settings.openaiReasoningEffort,
2073
- );
2474
+ const sessionReasoningEffort = existingSession.reasoningEffort;
2074
2475
  const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
2075
- const sessionLatencyMode = latencyModeForSession(existingSession.metadata, "standard");
2476
+ const sessionLatencyMode = existingSession.latencyMode;
2076
2477
  const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
2077
2478
  const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
2078
2479
  modelId: effectiveModel,
@@ -2119,60 +2520,96 @@ export async function acceptSessionUserMessageWithOutcome(
2119
2520
  session: existingSession,
2120
2521
  updates: input.mcpCredentialUpdates ?? [],
2121
2522
  });
2122
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings, {
2123
- subjectId: grant.subjectId,
2124
- });
2125
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
2523
+ const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
2524
+ const inheritedPersonalConnectionDelegations =
2525
+ connectionDelegationSource.kind === "turn"
2526
+ ? await getSessionTurnPersonalConnectionDelegations(
2527
+ db,
2528
+ workspaceId,
2529
+ connectionDelegationSource.sessionId,
2530
+ connectionDelegationSource.turnId,
2531
+ )
2532
+ : null;
2533
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
2126
2534
  db,
2127
2535
  workspaceId,
2128
- settings: runtimeSettings,
2129
- tools: existingSession.tools,
2130
- source: personalConnectionDelegationSourceForGrant(grant),
2131
- });
2132
- const { accepted, turn, interruptionCount, replay } = await postUserMessageTurn({
2133
- db,
2134
- bus,
2135
- workflowClient,
2136
2536
  settings,
2137
- accountId: grant.accountId,
2138
- workspaceId,
2139
- sessionId,
2140
- text: input.text,
2141
- annotations,
2142
- modelContext: input.modelContext ?? null,
2143
- resources: requestedResources,
2144
- model: input.model ?? null,
2145
- reasoningEffort: input.reasoningEffort ?? null,
2146
- latencyMode: input.latencyMode ?? null,
2147
- reasoningEffortFallback: sessionReasoningEffort,
2148
- turnExecutionPolicy,
2149
- mcpCredentialUpdates,
2150
- personalConnectionDelegations,
2151
- delivery: input.delivery ?? "send",
2152
- origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
2153
- actor: grant.subjectId,
2154
- ...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
2155
- ...(delegatedServiceInitiator
2537
+ inheritedPersonalConnectionDelegations
2156
2538
  ? {
2157
- commandActor: {
2158
- type: "service" as const,
2159
- subjectId: delegatedServiceInitiator.initiator.subjectId,
2160
- ...(delegatedServiceInitiator.initiator.label
2161
- ? { subjectLabel: delegatedServiceInitiator.initiator.label }
2162
- : {}),
2163
- context: delegatedServiceInitiator.context,
2164
- },
2539
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
2165
2540
  }
2166
- : {}),
2167
- ...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
2168
- ...(input.expectedDraftRevision !== undefined
2169
- ? { expectedDraftRevision: input.expectedDraftRevision }
2170
- : {}),
2171
- ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
2172
- recordAgentRunUsage: true,
2173
- ...(deps.schedulePromptPostCommit ? { schedulePostCommit: deps.schedulePromptPostCommit } : {}),
2541
+ : { subjectId: grant.subjectId },
2542
+ );
2543
+ const personalConnectionDelegations = await freezePersonalConnectionDelegations({
2544
+ db,
2545
+ workspaceId,
2546
+ settings: runtimeSettings,
2547
+ tools: existingSession.tools,
2548
+ resources: [...existingSession.resources, ...requestedResources],
2549
+ source: connectionDelegationSource,
2550
+ targetSessionId: sessionId,
2551
+ googleDrivePublicationEnabled:
2552
+ existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
2553
+ existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
2554
+ (!existingSession.firstPartyMcpPermissions?.length ||
2555
+ (existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
2556
+ existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
2557
+ atlassianEnabled:
2558
+ existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
2559
+ (!existingSession.firstPartyMcpPermissions?.length ||
2560
+ existingSession.firstPartyMcpPermissions.includes("connections:read")),
2561
+ ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
2174
2562
  });
2175
- return { accepted, turn, interruptionCount, replay };
2563
+ const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
2564
+ await postUserMessageTurn({
2565
+ db,
2566
+ bus,
2567
+ workflowClient,
2568
+ settings,
2569
+ accountId: grant.accountId,
2570
+ workspaceId,
2571
+ sessionId,
2572
+ text: input.text,
2573
+ annotations,
2574
+ modelContext: input.modelContext ?? null,
2575
+ resources: requestedResources,
2576
+ model: input.model ?? null,
2577
+ reasoningEffort: input.reasoningEffort ?? null,
2578
+ latencyMode: input.latencyMode ?? null,
2579
+ reasoningEffortFallback: sessionReasoningEffort,
2580
+ turnExecutionPolicy,
2581
+ mcpCredentialUpdates,
2582
+ personalConnectionDelegations,
2583
+ ...(input.personalResourceAttachment
2584
+ ? { personalResourceAttachment: input.personalResourceAttachment }
2585
+ : {}),
2586
+ delivery: input.delivery ?? "send",
2587
+ origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
2588
+ actor: grant.subjectId,
2589
+ ...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
2590
+ ...(delegatedServiceInitiator
2591
+ ? {
2592
+ commandActor: {
2593
+ type: "service" as const,
2594
+ subjectId: delegatedServiceInitiator.initiator.subjectId,
2595
+ ...(delegatedServiceInitiator.initiator.label
2596
+ ? { subjectLabel: delegatedServiceInitiator.initiator.label }
2597
+ : {}),
2598
+ context: delegatedServiceInitiator.context,
2599
+ },
2600
+ }
2601
+ : {}),
2602
+ ...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
2603
+ ...(input.expectedDraftRevision !== undefined
2604
+ ? { expectedDraftRevision: input.expectedDraftRevision }
2605
+ : {}),
2606
+ ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
2607
+ recordAgentRunUsage: true,
2608
+ ...(deps.schedulePromptPostCommit
2609
+ ? { schedulePostCommit: deps.schedulePromptPostCommit }
2610
+ : {}),
2611
+ });
2612
+ return { accepted, turn, draft, receipt, routing, interruptionCount, replay };
2176
2613
  }
2177
2614
 
2178
2615
  /** Backward-compatible entity-returning path used by existing REST callers. */
@@ -2185,17 +2622,14 @@ export async function acceptSessionUserMessage(
2185
2622
  ): Promise<{
2186
2623
  accepted: SessionEvent;
2187
2624
  turn: SessionTurn;
2625
+ receipt: SessionCommandReceipt;
2626
+ routing: SessionPromptRouting;
2188
2627
  interruptionCount: number;
2189
2628
  replay: boolean;
2190
2629
  }> {
2191
- const { accepted, turn, interruptionCount, replay } = await acceptSessionUserMessageWithOutcome(
2192
- deps,
2193
- grant,
2194
- workspaceId,
2195
- sessionId,
2196
- input,
2197
- );
2198
- return { accepted, turn, interruptionCount, replay };
2630
+ const { accepted, turn, receipt, routing, interruptionCount, replay } =
2631
+ await acceptSessionUserMessageWithOutcome(deps, grant, workspaceId, sessionId, input);
2632
+ return { accepted, turn, receipt, routing, interruptionCount, replay };
2199
2633
  }
2200
2634
 
2201
2635
  /**
@@ -2342,6 +2776,7 @@ function toolPolicyAuditSnapshot(
2342
2776
  kind: tool.kind,
2343
2777
  id: tool.id,
2344
2778
  ...(tool.optional === undefined ? {} : { optional: tool.optional }),
2779
+ ...(tool.eager === undefined ? {} : { eager: tool.eager }),
2345
2780
  }));
2346
2781
  const toolRefs = allToolRefs.slice(0, maxToolPolicyAuditRefs);
2347
2782
  return {
@@ -2384,6 +2819,8 @@ export async function updateSessionToolPolicy(
2384
2819
  requirePermission(grant, "sessions:control");
2385
2820
 
2386
2821
  const existingSession = await requireSession(deps.db, grant.workspaceId, sessionId);
2822
+ const workspace = await requireWorkspace(deps.db, grant.workspaceId);
2823
+ const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
2387
2824
  const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
2388
2825
  deps.db,
2389
2826
  grant.workspaceId,
@@ -2424,10 +2861,19 @@ export async function updateSessionToolPolicy(
2424
2861
  });
2425
2862
  }
2426
2863
  const workspaceDefaultTools = withFirstPartyTools(
2427
- withDefaultEnabledCapabilityMcpTools([], deps.settings, capabilityRuntimeSettings),
2864
+ withWorkspaceDefaultMcpTools(
2865
+ [],
2866
+ deps.settings,
2867
+ capabilityRuntimeSettings,
2868
+ workspaceSessionToolDefaults,
2869
+ ),
2428
2870
  runtimeSettings,
2429
2871
  );
2430
- const workspaceDefaultFirstPartyTools = [...deploymentFirstPartyMcpToolPolicy.default];
2872
+ const workspaceDefaultFirstPartyTools = [
2873
+ ...(workspaceSessionToolDefaults?.firstPartyMcpTools.filter((tool) =>
2874
+ deploymentFirstPartyMcpToolPolicy.allowed.includes(tool),
2875
+ ) ?? deploymentFirstPartyMcpToolPolicy.default),
2876
+ ];
2431
2877
  const events = await appendSessionEventsWithLockedSessionUpdate(
2432
2878
  deps.db,
2433
2879
  grant.workspaceId,
@@ -2451,10 +2897,11 @@ export async function updateSessionToolPolicy(
2451
2897
  const parentTracksWorkspaceDefaults = parent.toolPolicy?.mode === "workspace_default";
2452
2898
  const parentEffective = withFirstPartyTools(
2453
2899
  parentTracksWorkspaceDefaults
2454
- ? withDefaultEnabledCapabilityMcpTools(
2900
+ ? withWorkspaceDefaultMcpTools(
2455
2901
  availableToolRefs(parent.tools, runtimeSettings),
2456
2902
  deps.settings,
2457
2903
  runtimeSettings,
2904
+ workspaceSessionToolDefaults,
2458
2905
  )
2459
2906
  : parent.tools,
2460
2907
  runtimeSettings,