@opengeni/core 2.6.4 → 2.7.5-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.
- package/dist/access/index.d.ts +10 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/billing/limits.d.ts +5 -0
- package/dist/dependencies.d.ts +12 -1
- package/dist/domain/host-mcp-authority-source-admission.d.ts +10 -0
- package/dist/domain/packs.d.ts +1 -0
- package/dist/domain/pr-review.d.ts +1 -1
- package/dist/domain/product-integration-pack.d.ts +50 -0
- package/dist/domain/sessions.d.ts +25 -11
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2360 -687
- package/dist/index.js.map +1 -1
- package/dist/model-catalog.d.ts +105 -0
- package/dist/sandbox/fleet.d.ts +19 -0
- package/package.json +12 -10
- package/src/access/index.ts +25 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/session-commands.ts +2 -2
- package/src/billing/limits.ts +52 -22
- package/src/dependencies.ts +17 -1
- package/src/domain/capabilities.ts +19 -3
- package/src/domain/host-mcp-authority-source-admission.ts +25 -0
- package/src/domain/insights.ts +32 -0
- package/src/domain/packs.ts +48 -18
- package/src/domain/personal-connection-delegations.ts +14 -6
- package/src/domain/product-integration-pack.ts +494 -0
- package/src/domain/scheduled-tasks.ts +43 -1
- package/src/domain/sessions.ts +964 -386
- package/src/index.ts +4 -0
- package/src/model-catalog.ts +742 -0
- package/src/sandbox/fleet.ts +85 -23
- package/src/sandbox/routing.ts +6 -9
package/src/domain/sessions.ts
CHANGED
|
@@ -2,10 +2,13 @@ import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
|
|
|
2
2
|
import {
|
|
3
3
|
canonicalizeConfiguredModelId,
|
|
4
4
|
configuredAllowedModels,
|
|
5
|
+
ORGANIZATION_GATEWAY_MODEL_ID_PREFIX,
|
|
6
|
+
ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX,
|
|
5
7
|
resolveFirstPartyMcpToolPolicy,
|
|
6
8
|
policyProviderIdForModel,
|
|
7
9
|
resolveTurnExecutionPolicyV1,
|
|
8
10
|
WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
|
|
11
|
+
WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
|
|
9
12
|
XAI_SUBSCRIPTION_MODEL_ID_PREFIX,
|
|
10
13
|
type Settings,
|
|
11
14
|
} from "@opengeni/config";
|
|
@@ -17,12 +20,15 @@ import {
|
|
|
17
20
|
DraftTimelineAnnotations,
|
|
18
21
|
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
19
22
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
23
|
+
SessionSkills,
|
|
20
24
|
SessionSpawnDenial,
|
|
21
25
|
ServiceTurnInitiator,
|
|
22
26
|
ServiceTurnInitiatorContext,
|
|
23
27
|
evaluateWorkspaceModelPolicy,
|
|
24
28
|
normalizeAutomaticSessionTitle,
|
|
25
29
|
resolveWorkspaceSessionToolDefaults,
|
|
30
|
+
metadataWithTurnExecutionPolicyV1,
|
|
31
|
+
readTurnExecutionPolicyV1,
|
|
26
32
|
stableJson,
|
|
27
33
|
type AccessGrant,
|
|
28
34
|
type ComposerDraft,
|
|
@@ -60,8 +66,10 @@ import {
|
|
|
60
66
|
type XaiProviderAccountAuthoritySnapshotV1,
|
|
61
67
|
} from "@opengeni/contracts";
|
|
62
68
|
import {
|
|
69
|
+
assertExactNewSessionDraftInTransaction,
|
|
63
70
|
createSession,
|
|
64
71
|
createSessionWithIdempotencyKeyResult,
|
|
72
|
+
canonicalSessionCommandHash,
|
|
65
73
|
encryptVariableSetValue,
|
|
66
74
|
getAnySessionInGroup,
|
|
67
75
|
getEnrollment,
|
|
@@ -70,12 +78,13 @@ import {
|
|
|
70
78
|
getWorkspaceDefaultRigId,
|
|
71
79
|
listDistinctVariableSetSelectionsInGroup,
|
|
72
80
|
listDistinctRigVersionIdsInGroup,
|
|
81
|
+
listInstalledPortableSkills,
|
|
73
82
|
getSandbox,
|
|
74
83
|
getSession,
|
|
84
|
+
getInitializedSessionCreateReplay,
|
|
75
85
|
getSessionAuthorityProjection,
|
|
76
86
|
SessionIdConflictError,
|
|
77
87
|
NewSessionDraftConflictError,
|
|
78
|
-
getSessionSpawnDenialByIdempotencyKey,
|
|
79
88
|
getWorkspaceControlEvent,
|
|
80
89
|
getSessionLineage,
|
|
81
90
|
getSessionTurn,
|
|
@@ -88,6 +97,9 @@ import {
|
|
|
88
97
|
listSessionTurns,
|
|
89
98
|
listSessionMcpServersForChildInheritance,
|
|
90
99
|
requireSession,
|
|
100
|
+
setActiveSandbox,
|
|
101
|
+
setSubjectRlsContext,
|
|
102
|
+
replaySubmittedHumanPromptFromBoundaryReceipt,
|
|
91
103
|
submitHumanPromptInTransaction,
|
|
92
104
|
appendSessionEventsWithLockedSessionUpdate,
|
|
93
105
|
updateSessionTitleWithEvent,
|
|
@@ -127,8 +139,19 @@ import {
|
|
|
127
139
|
requireSessionAuthorization,
|
|
128
140
|
SessionAuthorizationDeniedError,
|
|
129
141
|
} from "../session-authorization";
|
|
130
|
-
import {
|
|
142
|
+
import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
|
|
143
|
+
import {
|
|
144
|
+
preflightCreateTimeSandboxTarget,
|
|
145
|
+
swapActiveSandbox,
|
|
146
|
+
type FleetContext,
|
|
147
|
+
} from "../sandbox/fleet";
|
|
131
148
|
import { managedSessionGroupBackend } from "../sandbox/runtime-settings";
|
|
149
|
+
import {
|
|
150
|
+
isWorkspaceCustomModelId,
|
|
151
|
+
lockActiveCustomModelForAdmission,
|
|
152
|
+
resolveWorkspaceCatalogSettings,
|
|
153
|
+
workspaceCustomModelReference,
|
|
154
|
+
} from "../model-catalog";
|
|
132
155
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
133
156
|
import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
|
|
134
157
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
@@ -158,6 +181,15 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
|
158
181
|
// Keep the durable snapshot below the shared event-preview array boundary so
|
|
159
182
|
// the generic lossy projection cannot silently rewrite this audit fact.
|
|
160
183
|
const maxToolPolicyAuditRefs = 40;
|
|
184
|
+
|
|
185
|
+
function isCatalogOverlayModel(modelId: string | null | undefined): boolean {
|
|
186
|
+
return (
|
|
187
|
+
modelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
|
|
188
|
+
modelId?.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) === true ||
|
|
189
|
+
modelId?.startsWith(ORGANIZATION_GATEWAY_MODEL_ID_PREFIX) === true ||
|
|
190
|
+
modelId?.startsWith(ORGANIZATION_OPENROUTER_MODEL_ID_PREFIX) === true
|
|
191
|
+
);
|
|
192
|
+
}
|
|
161
193
|
// RFC 9110 field-name token characters.
|
|
162
194
|
const sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
163
195
|
|
|
@@ -470,6 +502,7 @@ function validateSessionMcpServersForCreate(
|
|
|
470
502
|
const dbServers: CreateSessionMcpServerInput[] = [];
|
|
471
503
|
const metadata: SessionMcpServerMetadata[] = [];
|
|
472
504
|
for (const server of servers) {
|
|
505
|
+
assertHostMcpAuthoritySourceAdmissionEnabled(settings, server.connectionRef);
|
|
473
506
|
if (seenIds.has(server.id)) {
|
|
474
507
|
throw new HTTPException(422, {
|
|
475
508
|
message: `duplicate session MCP server id: ${server.id}`,
|
|
@@ -656,6 +689,16 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
656
689
|
/** Internal database-only composition seam. The exact session shell and this
|
|
657
690
|
* linkage commit together before its first event/turn can be initialized. */
|
|
658
691
|
beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
|
|
692
|
+
/** The custom workspace model was frozen by an earlier accepted boundary or
|
|
693
|
+
* inherited from an existing session, so retirement must not invalidate it. */
|
|
694
|
+
retainWorkspaceGatewayModel?: boolean;
|
|
695
|
+
/** Provider-neutral successor to retainWorkspaceGatewayModel. */
|
|
696
|
+
retainWorkspaceCustomModel?: boolean;
|
|
697
|
+
/** The selected workspace Gateway product is backed by a mutable custom row,
|
|
698
|
+
* rather than deployment-curated Gateway membership. */
|
|
699
|
+
workspaceGatewayCustomModel?: boolean;
|
|
700
|
+
/** Provider-neutral successor to workspaceGatewayCustomModel. */
|
|
701
|
+
workspaceCustomModel?: boolean;
|
|
659
702
|
accountId: string;
|
|
660
703
|
workspaceId: string;
|
|
661
704
|
visibility?: "user_private" | "workspace_shared";
|
|
@@ -683,7 +726,11 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
683
726
|
createdByActor?: Extract<SessionCommandActor, { type: "agent_attempt" }> | null;
|
|
684
727
|
// Ordered low-to-high precedence. Names/ids only; session.created never
|
|
685
728
|
// carries variable values.
|
|
686
|
-
variableSets?: Array<{
|
|
729
|
+
variableSets?: Array<{
|
|
730
|
+
id: string;
|
|
731
|
+
name: string;
|
|
732
|
+
scope: VariableSet["scope"];
|
|
733
|
+
}>;
|
|
687
734
|
// The rig + frozen active rig version resolved at create (M3). Both null ⇒ a
|
|
688
735
|
// rig-less session (byte-for-byte today's behavior). Frozen here so a later
|
|
689
736
|
// rig promote never moves an existing session's version.
|
|
@@ -725,6 +772,9 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
725
772
|
// session. Every caller repairs or re-delivers the winner's one atomic start;
|
|
726
773
|
// the durable initializer prevents duplicate events or turns.
|
|
727
774
|
createIdempotencyKey?: string | null;
|
|
775
|
+
// Exact explicit installed-Skill selection. The database stores this only as
|
|
776
|
+
// keyed-create identity; runtime behavior comes from the frozen Skill content.
|
|
777
|
+
selectedInstalledSkillIds?: string[];
|
|
728
778
|
// The shared-sandbox group this session's box joins (addendum 05 §D). Null/
|
|
729
779
|
// omitted ⇒ a singleton group (the new row's own id, today's 1:1 behavior); a
|
|
730
780
|
// shared/{groupId} spawn passes the resolved group so both run in ONE box.
|
|
@@ -735,11 +785,11 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
735
785
|
// OS-labeling surfaces honestly reflect the machine.
|
|
736
786
|
sandboxOs?: Session["sandboxOs"];
|
|
737
787
|
// Create-time machine targeting (A-2a, RACE-FREE): the enrolled machine (a
|
|
738
|
-
// sandbox id) to run this session on. When set,
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// target fails the create (422)
|
|
788
|
+
// sandbox id) to run this session on. When set, target liveness is preflighted
|
|
789
|
+
// before insertion, then the active-sandbox pointer is authority-checked and
|
|
790
|
+
// seeded (epoch-fenced) in the SAME transaction as the session row. The FIRST
|
|
791
|
+
// turn therefore routes to the chosen machine, while an invalid/unowned/offline
|
|
792
|
+
// target fails the create (422) without leaving a queued session shell.
|
|
743
793
|
// `workingDir` (optional) is the path/cwd base the chosen machine runs under,
|
|
744
794
|
// seeded alongside the pointer through the epoch-fenced CAS.
|
|
745
795
|
seedTargetSandbox?: {
|
|
@@ -775,16 +825,117 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
775
825
|
allowNestedAgentDepthIncrease?: boolean;
|
|
776
826
|
subjectId?: string | null;
|
|
777
827
|
}): Promise<CreateSessionOutcome> {
|
|
778
|
-
const sessionMetadata =
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
828
|
+
const sessionMetadata = metadataWithTurnExecutionPolicyV1(
|
|
829
|
+
{
|
|
830
|
+
...input.metadata,
|
|
831
|
+
model: input.model,
|
|
832
|
+
reasoningEffort: input.reasoningEffort,
|
|
833
|
+
...(input.latencyMode !== undefined ? { latencyMode: input.latencyMode } : {}),
|
|
834
|
+
},
|
|
835
|
+
input.turnExecutionPolicy,
|
|
836
|
+
);
|
|
784
837
|
const frozenCreatedByContext = freezeAgentChildAutomaticTitleInCreatorContext(
|
|
785
838
|
input.createdByContext,
|
|
786
839
|
input.initialAutomaticTitle,
|
|
787
840
|
);
|
|
841
|
+
const requiresActiveWorkspaceCustomModel =
|
|
842
|
+
(input.workspaceCustomModel === true || input.workspaceGatewayCustomModel === true) &&
|
|
843
|
+
input.retainWorkspaceCustomModel !== true &&
|
|
844
|
+
input.retainWorkspaceGatewayModel !== true;
|
|
845
|
+
const seedTargetForNewSession = input.seedTargetSandbox ?? null;
|
|
846
|
+
const preflightTarget = seedTargetForNewSession
|
|
847
|
+
? await preflightCreateTimeSandboxTarget(
|
|
848
|
+
{
|
|
849
|
+
db: input.db,
|
|
850
|
+
settings: seedTargetForNewSession.settings,
|
|
851
|
+
bus: input.bus,
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
accountId: input.accountId,
|
|
855
|
+
workspaceId: input.workspaceId,
|
|
856
|
+
...(seedTargetForNewSession.resourceSubjectId
|
|
857
|
+
? { subjectId: seedTargetForNewSession.resourceSubjectId }
|
|
858
|
+
: {}),
|
|
859
|
+
},
|
|
860
|
+
seedTargetForNewSession.sandboxId,
|
|
861
|
+
seedTargetForNewSession.workingDir ?? null,
|
|
862
|
+
)
|
|
863
|
+
: null;
|
|
864
|
+
const targetPreflightFailureMessage =
|
|
865
|
+
preflightTarget && !preflightTarget.ok
|
|
866
|
+
? `cannot target sandbox ${seedTargetForNewSession!.sandboxId}: ${preflightTarget.reason}`
|
|
867
|
+
: null;
|
|
868
|
+
let targetSeededBeforeCreateCommit = false;
|
|
869
|
+
const beforeCreateCommit =
|
|
870
|
+
requiresActiveWorkspaceCustomModel ||
|
|
871
|
+
input.consumeNewSessionDraft ||
|
|
872
|
+
input.beforeCreateCommit ||
|
|
873
|
+
preflightTarget ||
|
|
874
|
+
targetPreflightFailureMessage
|
|
875
|
+
? async (tx: Database, sessionId: string, context?: { created: boolean }): Promise<void> => {
|
|
876
|
+
// A committed keyed replay already crossed this fence when its shell
|
|
877
|
+
// was first accepted. Revalidate only the transaction inserting a new
|
|
878
|
+
// session, while still running caller linkage on every replay.
|
|
879
|
+
if (targetPreflightFailureMessage && context?.created !== false) {
|
|
880
|
+
throw new HTTPException(422, {
|
|
881
|
+
message: targetPreflightFailureMessage,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (requiresActiveWorkspaceCustomModel && context?.created !== false) {
|
|
885
|
+
const reference = {
|
|
886
|
+
scope: input.turnExecutionPolicy.providerId.startsWith("organization-")
|
|
887
|
+
? ("organization" as const)
|
|
888
|
+
: ("workspace" as const),
|
|
889
|
+
providerKind: input.turnExecutionPolicy.providerId.includes("openrouter")
|
|
890
|
+
? ("openrouter" as const)
|
|
891
|
+
: ("vercel_gateway" as const),
|
|
892
|
+
upstreamModelId: input.turnExecutionPolicy.upstreamModelId,
|
|
893
|
+
};
|
|
894
|
+
const active = await lockActiveCustomModelForAdmission(tx, {
|
|
895
|
+
accountId: input.accountId,
|
|
896
|
+
workspaceId: input.workspaceId,
|
|
897
|
+
reference,
|
|
898
|
+
});
|
|
899
|
+
if (!active) {
|
|
900
|
+
throw new HTTPException(422, {
|
|
901
|
+
message: `model is not available: ${input.model}`,
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
// Reject an already-stale browser draft before the newly inserted
|
|
906
|
+
// shell can commit. The initializer repeats this exact check while
|
|
907
|
+
// consuming the draft after it installs the first runnable unit.
|
|
908
|
+
if (input.consumeNewSessionDraft && context?.created !== false) {
|
|
909
|
+
await setSubjectRlsContext(tx, input.consumeNewSessionDraft.subjectId);
|
|
910
|
+
await assertExactNewSessionDraftInTransaction(tx, {
|
|
911
|
+
workspaceId: input.workspaceId,
|
|
912
|
+
subjectId: input.consumeNewSessionDraft.subjectId,
|
|
913
|
+
expectedRevision: input.consumeNewSessionDraft.expectedRevision,
|
|
914
|
+
expectedSnapshot: input.consumeNewSessionDraft.expectedSnapshot,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
if (preflightTarget?.ok && context?.created !== false) {
|
|
918
|
+
const seeded = await setActiveSandbox(tx, {
|
|
919
|
+
accountId: input.accountId,
|
|
920
|
+
workspaceId: input.workspaceId,
|
|
921
|
+
sessionId,
|
|
922
|
+
targetSandboxId: preflightTarget.targetSandboxId,
|
|
923
|
+
expectedEpoch: 0,
|
|
924
|
+
...(seedTargetForNewSession?.resourceSubjectId
|
|
925
|
+
? { subjectId: seedTargetForNewSession.resourceSubjectId }
|
|
926
|
+
: {}),
|
|
927
|
+
workingDir: preflightTarget.workingDir,
|
|
928
|
+
});
|
|
929
|
+
if (!seeded.swapped) {
|
|
930
|
+
throw new HTTPException(422, {
|
|
931
|
+
message: `cannot target sandbox ${seedTargetForNewSession!.sandboxId}: target authority changed during session creation`,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
targetSeededBeforeCreateCommit = true;
|
|
935
|
+
}
|
|
936
|
+
await input.beforeCreateCommit?.(tx, sessionId);
|
|
937
|
+
}
|
|
938
|
+
: undefined;
|
|
788
939
|
// Keyed creation is intentionally handled only by the database admission
|
|
789
940
|
// transaction below. Its workspace/key lock replays either the successful
|
|
790
941
|
// session or the committed denial atomically; an application-side lookup
|
|
@@ -820,6 +971,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
820
971
|
policyRole: input.policyRole ?? null,
|
|
821
972
|
parentSessionId: input.parentSessionId ?? null,
|
|
822
973
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
974
|
+
selectedInstalledSkillIds: input.selectedInstalledSkillIds ?? [],
|
|
823
975
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
824
976
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
825
977
|
mcpServers: input.mcpServers ?? [],
|
|
@@ -834,15 +986,29 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
834
986
|
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
835
987
|
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
836
988
|
subjectId: input.subjectId ?? null,
|
|
837
|
-
...(
|
|
989
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
838
990
|
});
|
|
839
991
|
if (keyedResult.denied) {
|
|
840
992
|
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
|
|
841
993
|
}
|
|
842
994
|
const { session: keyed, created } = keyedResult;
|
|
843
995
|
if (!created) {
|
|
996
|
+
const persistedPolicy = readTurnExecutionPolicyV1(keyed.metadata);
|
|
844
997
|
const finished = await finishStartSession(
|
|
845
|
-
keyed.temporalWorkflowId
|
|
998
|
+
keyed.temporalWorkflowId
|
|
999
|
+
? {
|
|
1000
|
+
...input,
|
|
1001
|
+
seedTargetSandbox: null,
|
|
1002
|
+
...(persistedPolicy.kind === "valid"
|
|
1003
|
+
? { turnExecutionPolicy: persistedPolicy.policy }
|
|
1004
|
+
: {}),
|
|
1005
|
+
}
|
|
1006
|
+
: {
|
|
1007
|
+
...input,
|
|
1008
|
+
...(persistedPolicy.kind === "valid"
|
|
1009
|
+
? { turnExecutionPolicy: persistedPolicy.policy }
|
|
1010
|
+
: {}),
|
|
1011
|
+
},
|
|
846
1012
|
keyed,
|
|
847
1013
|
);
|
|
848
1014
|
return {
|
|
@@ -852,7 +1018,10 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
852
1018
|
changed: finished.changed,
|
|
853
1019
|
};
|
|
854
1020
|
}
|
|
855
|
-
const finished = await finishStartSession(
|
|
1021
|
+
const finished = await finishStartSession(
|
|
1022
|
+
targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
|
|
1023
|
+
keyed,
|
|
1024
|
+
);
|
|
856
1025
|
return {
|
|
857
1026
|
session: finished.session,
|
|
858
1027
|
outcome: "created",
|
|
@@ -905,7 +1074,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
905
1074
|
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
906
1075
|
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
907
1076
|
subjectId: input.subjectId ?? null,
|
|
908
|
-
...(
|
|
1077
|
+
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
909
1078
|
});
|
|
910
1079
|
} catch (error) {
|
|
911
1080
|
if (error instanceof SessionSpawnDeniedDbError) {
|
|
@@ -913,7 +1082,10 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
913
1082
|
}
|
|
914
1083
|
throw error;
|
|
915
1084
|
}
|
|
916
|
-
const finished = await finishStartSession(
|
|
1085
|
+
const finished = await finishStartSession(
|
|
1086
|
+
targetSeededBeforeCreateCommit ? { ...input, seedTargetSandbox: null } : input,
|
|
1087
|
+
session,
|
|
1088
|
+
);
|
|
917
1089
|
return {
|
|
918
1090
|
session: finished.session,
|
|
919
1091
|
outcome: "created",
|
|
@@ -951,7 +1123,11 @@ async function finishStartSession(
|
|
|
951
1123
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
952
1124
|
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
953
1125
|
sandboxBackend: Settings["sandboxBackend"];
|
|
954
|
-
variableSets?: Array<{
|
|
1126
|
+
variableSets?: Array<{
|
|
1127
|
+
id: string;
|
|
1128
|
+
name: string;
|
|
1129
|
+
scope: VariableSet["scope"];
|
|
1130
|
+
}>;
|
|
955
1131
|
goal?: GoalSpec | null;
|
|
956
1132
|
initialAutomaticTitle?: string | null;
|
|
957
1133
|
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
@@ -984,15 +1160,12 @@ async function finishStartSession(
|
|
|
984
1160
|
): Promise<{ session: CreateSessionResponse; changed: boolean }> {
|
|
985
1161
|
// Create-time machine targeting (A-2a): seed the active-sandbox pointer BEFORE
|
|
986
1162
|
// the atomic initial turn transaction, so the FIRST turn routes to the chosen
|
|
987
|
-
// machine.
|
|
1163
|
+
// machine. Home backend and active route are independent: a backend:none
|
|
1164
|
+
// session has no managed home but may still attach a valid Connected Machine.
|
|
1165
|
+
// swapActiveSandbox does
|
|
988
1166
|
// the same ownership+liveness validation as the live swap; an invalid/unowned/
|
|
989
1167
|
// offline target FAILS the create (422) — never a silent fall-back to the box.
|
|
990
1168
|
if (input.seedTargetSandbox) {
|
|
991
|
-
if (session.sandboxBackend === "none") {
|
|
992
|
-
throw new HTTPException(422, {
|
|
993
|
-
message: "cannot target a machine for a session with no sandbox (backend: none)",
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
1169
|
const ctx: FleetContext = {
|
|
997
1170
|
accountId: session.accountId,
|
|
998
1171
|
workspaceId: session.workspaceId,
|
|
@@ -1137,9 +1310,6 @@ export function canonicalConfiguredModel(
|
|
|
1137
1310
|
) {
|
|
1138
1311
|
return canonicalModel;
|
|
1139
1312
|
}
|
|
1140
|
-
if (canonicalModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX)) {
|
|
1141
|
-
return canonicalModel;
|
|
1142
|
-
}
|
|
1143
1313
|
throw new HTTPException(422, { message: `model is not available: ${model}` });
|
|
1144
1314
|
}
|
|
1145
1315
|
|
|
@@ -1245,7 +1415,17 @@ export async function requireQueuedTurnForApi(
|
|
|
1245
1415
|
* `session_send_message` tool so the two surfaces cannot drift. Callers own
|
|
1246
1416
|
* resource/tool validation and the per-message usage limit before calling.
|
|
1247
1417
|
*/
|
|
1248
|
-
|
|
1418
|
+
type PostUserMessageTurnResult = {
|
|
1419
|
+
accepted: SessionEvent;
|
|
1420
|
+
turn: SessionTurn;
|
|
1421
|
+
draft: ComposerDraft | null;
|
|
1422
|
+
receipt: SessionCommandReceipt;
|
|
1423
|
+
routing: SessionPromptRouting;
|
|
1424
|
+
interruptionCount: number;
|
|
1425
|
+
replay: boolean;
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
type PostUserMessageTurnInput = {
|
|
1249
1429
|
db: Database;
|
|
1250
1430
|
bus: EventBus;
|
|
1251
1431
|
workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
|
|
@@ -1273,28 +1453,143 @@ export async function postUserMessageTurn(input: {
|
|
|
1273
1453
|
commandActor?: SessionCommandActor;
|
|
1274
1454
|
controlEtag?: string | null;
|
|
1275
1455
|
expectedDraftRevision?: number | null;
|
|
1456
|
+
boundaryRequestHash?: string;
|
|
1276
1457
|
reasoningEffortFallback?: Settings["openaiReasoningEffort"];
|
|
1277
1458
|
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
1278
1459
|
recordAgentRunUsage?: boolean;
|
|
1279
1460
|
schedulePostCommit?: (task: () => Promise<void>) => void;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
function finalizePostUserMessageTurn(
|
|
1464
|
+
input: Pick<
|
|
1465
|
+
PostUserMessageTurnInput,
|
|
1466
|
+
| "db"
|
|
1467
|
+
| "bus"
|
|
1468
|
+
| "workflowClient"
|
|
1469
|
+
| "accountId"
|
|
1470
|
+
| "workspaceId"
|
|
1471
|
+
| "sessionId"
|
|
1472
|
+
| "delivery"
|
|
1473
|
+
| "schedulePostCommit"
|
|
1474
|
+
>,
|
|
1475
|
+
result: Awaited<ReturnType<typeof submitHumanPromptInTransaction>>,
|
|
1476
|
+
): PostUserMessageTurnResult {
|
|
1477
|
+
const { db, bus, workflowClient, accountId, workspaceId, sessionId } = input;
|
|
1478
|
+
const postCommitTask = async () => {
|
|
1479
|
+
await Promise.all([
|
|
1480
|
+
(async () => {
|
|
1481
|
+
try {
|
|
1482
|
+
await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
|
|
1483
|
+
if (result.workspaceControlEventId) {
|
|
1484
|
+
const controlEvent = await getWorkspaceControlEvent(
|
|
1485
|
+
db,
|
|
1486
|
+
workspaceId,
|
|
1487
|
+
result.workspaceControlEventId,
|
|
1488
|
+
);
|
|
1489
|
+
if (!controlEvent) {
|
|
1490
|
+
throw new Error(
|
|
1491
|
+
`Committed workspace control event disappeared: ${result.workspaceControlEventId}`,
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
|
|
1495
|
+
}
|
|
1496
|
+
} catch {
|
|
1497
|
+
console.warn("[sessions] prompt event fanout failed; durable rows remain replayable", {
|
|
1498
|
+
errorClass: "PromptEventFanoutOperationError",
|
|
1499
|
+
errorCode: "session_prompt_event_fanout_failed",
|
|
1500
|
+
origin: "core",
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
})(),
|
|
1504
|
+
(async () => {
|
|
1505
|
+
try {
|
|
1506
|
+
await workflowClient.wakeSessionWorkflow({
|
|
1507
|
+
accountId,
|
|
1508
|
+
workspaceId,
|
|
1509
|
+
sessionId,
|
|
1510
|
+
workflowId: result.turn.temporalWorkflowId,
|
|
1511
|
+
wakeRevision: result.wakeRevision,
|
|
1512
|
+
...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
|
|
1513
|
+
? { interruptionRequested: true }
|
|
1514
|
+
: {}),
|
|
1515
|
+
});
|
|
1516
|
+
} catch {
|
|
1517
|
+
console.warn("[sessions] workflow wake failed; durable outbox will retry", {
|
|
1518
|
+
errorClass: "WorkflowWakeOperationError",
|
|
1519
|
+
errorCode: "session_workflow_wake_failed",
|
|
1520
|
+
origin: "core",
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
})(),
|
|
1524
|
+
]);
|
|
1525
|
+
};
|
|
1526
|
+
const schedulePostCommit =
|
|
1527
|
+
input.schedulePostCommit ??
|
|
1528
|
+
((task: () => Promise<void>) => {
|
|
1529
|
+
void task();
|
|
1530
|
+
});
|
|
1531
|
+
try {
|
|
1532
|
+
schedulePostCommit(postCommitTask);
|
|
1533
|
+
} catch {
|
|
1534
|
+
console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
|
|
1535
|
+
errorClass: "PromptPostCommitScheduleError",
|
|
1536
|
+
errorCode: "session_prompt_post_commit_schedule_failed",
|
|
1537
|
+
origin: "core",
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
accepted: result.accepted,
|
|
1542
|
+
turn: result.turn,
|
|
1543
|
+
receipt: {
|
|
1544
|
+
id: result.receipt.id,
|
|
1545
|
+
action: result.receipt.action,
|
|
1546
|
+
operationKey: result.receipt.operationKey,
|
|
1547
|
+
targetSessionId: result.receipt.targetSessionId,
|
|
1548
|
+
targetTurnId: result.receipt.targetTurnId,
|
|
1549
|
+
appliedControlRevision: result.receipt.appliedControlRevision,
|
|
1550
|
+
appliedQueueVersion: result.receipt.appliedQueueVersion,
|
|
1551
|
+
appliedTurnVersion: result.receipt.appliedTurnVersion,
|
|
1552
|
+
appliedDraftRevision: result.receipt.appliedDraftRevision,
|
|
1553
|
+
createdAt: result.receipt.createdAt.toISOString(),
|
|
1554
|
+
},
|
|
1555
|
+
routing: result.routing,
|
|
1556
|
+
draft: result.draft
|
|
1557
|
+
? {
|
|
1558
|
+
revision: result.draft.revision,
|
|
1559
|
+
text: result.draft.text,
|
|
1560
|
+
annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
|
|
1561
|
+
resources: result.draft.resources as ResourceRef[],
|
|
1562
|
+
model: result.draft.model,
|
|
1563
|
+
reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
|
|
1564
|
+
latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
|
|
1565
|
+
sourceTurnId: result.draft.sourceTurnId,
|
|
1566
|
+
sourceTurnVersion: result.draft.sourceTurnVersion,
|
|
1567
|
+
updatedAt: result.draft.updatedAt.toISOString(),
|
|
1568
|
+
}
|
|
1569
|
+
: null,
|
|
1570
|
+
interruptionCount: result.interruptionCount,
|
|
1571
|
+
replay: result.replay,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
export async function postUserMessageTurn(
|
|
1576
|
+
input: PostUserMessageTurnInput,
|
|
1577
|
+
): Promise<PostUserMessageTurnResult> {
|
|
1578
|
+
const { db, settings, accountId, workspaceId, sessionId } = input;
|
|
1290
1579
|
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
1291
1580
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
1292
1581
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
1293
1582
|
// model inherits the session's model downstream (always a configured id).
|
|
1294
1583
|
assertConfiguredModel(settings, requestedModel);
|
|
1295
|
-
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
1296
1584
|
const sessionForModelGate = await requireSession(db, workspaceId, sessionId);
|
|
1297
1585
|
const effectiveModelForGate = requestedModel ?? sessionForModelGate.model;
|
|
1586
|
+
const freshWorkspaceCustomModel =
|
|
1587
|
+
requestedModel !== null &&
|
|
1588
|
+
isWorkspaceCustomModelId(settings, requestedModel) &&
|
|
1589
|
+
requestedModel !== sessionForModelGate.model
|
|
1590
|
+
? requestedModel
|
|
1591
|
+
: null;
|
|
1592
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, effectiveModelForGate);
|
|
1298
1593
|
try {
|
|
1299
1594
|
assertSessionAllowsProductModel(sessionForModelGate, effectiveModelForGate);
|
|
1300
1595
|
} catch (error) {
|
|
@@ -1329,6 +1624,9 @@ export async function postUserMessageTurn(input: {
|
|
|
1329
1624
|
subjectId: input.actor ?? accountId,
|
|
1330
1625
|
},
|
|
1331
1626
|
operationKey,
|
|
1627
|
+
...(input.boundaryRequestHash
|
|
1628
|
+
? { boundaryRequestHash: input.boundaryRequestHash }
|
|
1629
|
+
: {}),
|
|
1332
1630
|
delivery: input.delivery ?? "send",
|
|
1333
1631
|
controlEtag: input.controlEtag ?? null,
|
|
1334
1632
|
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
@@ -1356,6 +1654,29 @@ export async function postUserMessageTurn(input: {
|
|
|
1356
1654
|
}
|
|
1357
1655
|
: {}),
|
|
1358
1656
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
1657
|
+
...(freshWorkspaceCustomModel
|
|
1658
|
+
? {
|
|
1659
|
+
beforeFreshPromptCommit: async (tx: Database): Promise<void> => {
|
|
1660
|
+
const reference = workspaceCustomModelReference(
|
|
1661
|
+
settings,
|
|
1662
|
+
freshWorkspaceCustomModel,
|
|
1663
|
+
);
|
|
1664
|
+
if (!reference) {
|
|
1665
|
+
throw new Error("workspace custom model reference disappeared");
|
|
1666
|
+
}
|
|
1667
|
+
const active = await lockActiveCustomModelForAdmission(tx, {
|
|
1668
|
+
accountId,
|
|
1669
|
+
workspaceId,
|
|
1670
|
+
reference,
|
|
1671
|
+
});
|
|
1672
|
+
if (!active) {
|
|
1673
|
+
throw new HTTPException(422, {
|
|
1674
|
+
message: `model is not available: ${freshWorkspaceCustomModel}`,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
},
|
|
1678
|
+
}
|
|
1679
|
+
: {}),
|
|
1359
1680
|
controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
|
|
1360
1681
|
}),
|
|
1361
1682
|
),
|
|
@@ -1386,125 +1707,31 @@ export async function postUserMessageTurn(input: {
|
|
|
1386
1707
|
}
|
|
1387
1708
|
throw error;
|
|
1388
1709
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
})(),
|
|
1415
|
-
(async () => {
|
|
1416
|
-
try {
|
|
1417
|
-
await workflowClient.wakeSessionWorkflow({
|
|
1418
|
-
accountId,
|
|
1419
|
-
workspaceId,
|
|
1420
|
-
sessionId,
|
|
1421
|
-
workflowId: result.turn.temporalWorkflowId,
|
|
1422
|
-
wakeRevision: result.wakeRevision,
|
|
1423
|
-
...((input.delivery ?? "send") === "steer" || result.interruptionCount > 0
|
|
1424
|
-
? { interruptionRequested: true }
|
|
1425
|
-
: {}),
|
|
1426
|
-
});
|
|
1427
|
-
} catch {
|
|
1428
|
-
console.warn("[sessions] workflow wake failed; durable outbox will retry", {
|
|
1429
|
-
errorClass: "WorkflowWakeOperationError",
|
|
1430
|
-
errorCode: "session_workflow_wake_failed",
|
|
1431
|
-
origin: "core",
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
})(),
|
|
1435
|
-
]);
|
|
1436
|
-
};
|
|
1437
|
-
const schedulePostCommit =
|
|
1438
|
-
input.schedulePostCommit ??
|
|
1439
|
-
((task: () => Promise<void>) => {
|
|
1440
|
-
void task();
|
|
1441
|
-
});
|
|
1442
|
-
try {
|
|
1443
|
-
schedulePostCommit(postCommitTask);
|
|
1444
|
-
} catch {
|
|
1445
|
-
console.warn("[sessions] prompt post-commit scheduling failed; durable recovery remains", {
|
|
1446
|
-
errorClass: "PromptPostCommitScheduleError",
|
|
1447
|
-
errorCode: "session_prompt_post_commit_schedule_failed",
|
|
1448
|
-
origin: "core",
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
return {
|
|
1452
|
-
accepted: result.accepted,
|
|
1453
|
-
turn: result.turn,
|
|
1454
|
-
receipt: {
|
|
1455
|
-
id: result.receipt.id,
|
|
1456
|
-
action: result.receipt.action,
|
|
1457
|
-
operationKey: result.receipt.operationKey,
|
|
1458
|
-
targetSessionId: result.receipt.targetSessionId,
|
|
1459
|
-
targetTurnId: result.receipt.targetTurnId,
|
|
1460
|
-
appliedControlRevision: result.receipt.appliedControlRevision,
|
|
1461
|
-
appliedQueueVersion: result.receipt.appliedQueueVersion,
|
|
1462
|
-
appliedTurnVersion: result.receipt.appliedTurnVersion,
|
|
1463
|
-
appliedDraftRevision: result.receipt.appliedDraftRevision,
|
|
1464
|
-
createdAt: result.receipt.createdAt.toISOString(),
|
|
1465
|
-
},
|
|
1466
|
-
routing: result.routing,
|
|
1467
|
-
draft: result.draft
|
|
1468
|
-
? {
|
|
1469
|
-
revision: result.draft.revision,
|
|
1470
|
-
text: result.draft.text,
|
|
1471
|
-
annotations: DraftTimelineAnnotations.parse(result.draft.annotations),
|
|
1472
|
-
resources: result.draft.resources as ResourceRef[],
|
|
1473
|
-
model: result.draft.model,
|
|
1474
|
-
reasoningEffort: result.draft.reasoningEffort as ReasoningEffort,
|
|
1475
|
-
latencyMode: result.draft.latencyMode as ComposerDraft["latencyMode"],
|
|
1476
|
-
sourceTurnId: result.draft.sourceTurnId,
|
|
1477
|
-
sourceTurnVersion: result.draft.sourceTurnVersion,
|
|
1478
|
-
updatedAt: result.draft.updatedAt.toISOString(),
|
|
1479
|
-
}
|
|
1480
|
-
: null,
|
|
1481
|
-
interruptionCount: result.interruptionCount,
|
|
1482
|
-
replay: result.replay,
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
/**
|
|
1487
|
-
* Full create-session flow shared by `POST /sessions` and the first-party MCP
|
|
1488
|
-
* `session_create` tool: payload validation, resource/tool/variableSet
|
|
1489
|
-
* checks, usage limits, session start, and usage recording. `rawPayload` is
|
|
1490
|
-
* the unparsed request body so absent-vs-empty execution-context fields keep
|
|
1491
|
-
* their meaning: a child inherits omitted resources/tools/mcpServers from its
|
|
1492
|
-
* trusted immediate parent, while explicit arrays (including []) win. A
|
|
1493
|
-
* top-level create with omitted tools applies workspace-default capability MCPs.
|
|
1494
|
-
*/
|
|
1495
|
-
export function resolveChildGoalFromAcceptedSnapshot(
|
|
1496
|
-
goal: GoalSpec,
|
|
1497
|
-
parentGoalSnapshot: SessionGoalSnapshot,
|
|
1498
|
-
): GoalSpec {
|
|
1499
|
-
const inheritedRootConstraints =
|
|
1500
|
-
parentGoalSnapshot.state === "none" ? [] : parentGoalSnapshot.rootConstraints;
|
|
1501
|
-
const requestedRootConstraints = goal.rootConstraints;
|
|
1502
|
-
if (
|
|
1503
|
-
requestedRootConstraints?.some((constraint) => !inheritedRootConstraints.includes(constraint))
|
|
1504
|
-
) {
|
|
1505
|
-
throw new Error(
|
|
1506
|
-
"child goal rootConstraints must be an exact subset of the calling turn's frozen root constraints",
|
|
1507
|
-
);
|
|
1710
|
+
return finalizePostUserMessageTurn(input, result);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Full create-session flow shared by `POST /sessions` and the first-party MCP
|
|
1715
|
+
* `session_create` tool: payload validation, resource/tool/variableSet
|
|
1716
|
+
* checks, usage limits, session start, and usage recording. `rawPayload` is
|
|
1717
|
+
* the unparsed request body so absent-vs-empty execution-context fields keep
|
|
1718
|
+
* their meaning: a child inherits omitted resources/tools/mcpServers from its
|
|
1719
|
+
* trusted immediate parent, while explicit arrays (including []) win. A
|
|
1720
|
+
* top-level create with omitted tools applies workspace-default capability MCPs.
|
|
1721
|
+
*/
|
|
1722
|
+
export function resolveChildGoalFromAcceptedSnapshot(
|
|
1723
|
+
goal: GoalSpec,
|
|
1724
|
+
parentGoalSnapshot: SessionGoalSnapshot,
|
|
1725
|
+
): GoalSpec {
|
|
1726
|
+
const inheritedRootConstraints =
|
|
1727
|
+
parentGoalSnapshot.state === "none" ? [] : parentGoalSnapshot.rootConstraints;
|
|
1728
|
+
const requestedRootConstraints = goal.rootConstraints;
|
|
1729
|
+
if (
|
|
1730
|
+
requestedRootConstraints?.some((constraint) => !inheritedRootConstraints.includes(constraint))
|
|
1731
|
+
) {
|
|
1732
|
+
throw new Error(
|
|
1733
|
+
"child goal rootConstraints must be an exact subset of the calling turn's frozen root constraints",
|
|
1734
|
+
);
|
|
1508
1735
|
}
|
|
1509
1736
|
return {
|
|
1510
1737
|
...goal,
|
|
@@ -1532,16 +1759,84 @@ export function resolveSessionCreateVisibility(input: {
|
|
|
1532
1759
|
return input.requestedVisibility === "private" ? "user_private" : "workspace_shared";
|
|
1533
1760
|
}
|
|
1534
1761
|
|
|
1762
|
+
async function resolveWorkspaceModelBoundarySettings(
|
|
1763
|
+
deps: Pick<ApiRouteDeps, "db" | "settings" | "catalogSourceSettings">,
|
|
1764
|
+
grant: AccessGrant,
|
|
1765
|
+
workspaceId: string,
|
|
1766
|
+
modelIds: readonly (string | null | undefined)[],
|
|
1767
|
+
retainedProductModelId?: string | null,
|
|
1768
|
+
): Promise<Settings> {
|
|
1769
|
+
const retainedCatalogModel = isCatalogOverlayModel(retainedProductModelId);
|
|
1770
|
+
if (deps.catalogSourceSettings) {
|
|
1771
|
+
// The adapter already resolved one exact workspace catalog snapshot for
|
|
1772
|
+
// this request. Preserve it for fresh selections, but an existing session
|
|
1773
|
+
// may name a retired custom model that the active-only adapter snapshot
|
|
1774
|
+
// intentionally omitted. Re-open only the unoverlaid source for that
|
|
1775
|
+
// retention lookup; never feed the synthetic workspace provider back
|
|
1776
|
+
// through deployment validation.
|
|
1777
|
+
if (!retainedCatalogModel) return deps.settings;
|
|
1778
|
+
}
|
|
1779
|
+
const needsWorkspaceResolution =
|
|
1780
|
+
deps.settings.modelCatalogSource === "database" || modelIds.some(isCatalogOverlayModel);
|
|
1781
|
+
if (!needsWorkspaceResolution) return deps.settings;
|
|
1782
|
+
return (
|
|
1783
|
+
await resolveWorkspaceCatalogSettings(deps.db, deps.catalogSourceSettings ?? deps.settings, {
|
|
1784
|
+
accountId: grant.accountId,
|
|
1785
|
+
workspaceId,
|
|
1786
|
+
...(retainedProductModelId !== undefined ? { retainedProductModelId } : {}),
|
|
1787
|
+
})
|
|
1788
|
+
).settings;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
async function withSessionCreateUsageRecording(input: {
|
|
1792
|
+
deps: ApiRouteDeps;
|
|
1793
|
+
grant: AccessGrant;
|
|
1794
|
+
workspaceId: string;
|
|
1795
|
+
startMode: "realtime" | undefined;
|
|
1796
|
+
origin: "system" | "user";
|
|
1797
|
+
createOutcome: CreateSessionOutcome;
|
|
1798
|
+
}): Promise<CreateSessionRequestOutcome> {
|
|
1799
|
+
let usageRecording: CreateSessionRequestOutcome["usageRecording"] = "recorded";
|
|
1800
|
+
if (input.startMode !== "realtime") {
|
|
1801
|
+
try {
|
|
1802
|
+
await recordWorkspaceUsage(input.deps, {
|
|
1803
|
+
accountId: input.grant.accountId,
|
|
1804
|
+
workspaceId: input.workspaceId,
|
|
1805
|
+
subjectId: input.grant.subjectId,
|
|
1806
|
+
eventType: "agent_run.created",
|
|
1807
|
+
quantity: 1,
|
|
1808
|
+
unit: "run",
|
|
1809
|
+
sourceResourceType: "session",
|
|
1810
|
+
sourceResourceId: input.createOutcome.session.id,
|
|
1811
|
+
sessionId: input.createOutcome.session.id,
|
|
1812
|
+
initiator: input.createOutcome.session.createdBy,
|
|
1813
|
+
initiatorContext: input.createOutcome.session.createdByContext,
|
|
1814
|
+
origin: input.origin,
|
|
1815
|
+
idempotencyKey: `agent_run.created:${input.workspaceId}:${input.createOutcome.session.id}`,
|
|
1816
|
+
});
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
usageRecording = "failed";
|
|
1819
|
+
reportSessionUsageRecordingFailure(error);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return { ...input.createOutcome, usageRecording };
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1535
1825
|
export async function createSessionForRequestWithOutcome(
|
|
1536
|
-
|
|
1826
|
+
unresolvedDeps: ApiRouteDeps,
|
|
1537
1827
|
grant: AccessGrant,
|
|
1538
1828
|
workspaceId: string,
|
|
1539
1829
|
rawPayload: unknown,
|
|
1540
1830
|
authorization?: AccessGrantAuthorization,
|
|
1541
1831
|
agentChildPresentation?: AgentChildSessionCreatePresentation,
|
|
1542
1832
|
): Promise<CreateSessionRequestOutcome> {
|
|
1543
|
-
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1544
1833
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
1834
|
+
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
1835
|
+
throw new HTTPException(422, {
|
|
1836
|
+
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
const db = unresolvedDeps.db;
|
|
1545
1840
|
const visibilityProvided = hasOwnProperty(rawPayload, "visibility");
|
|
1546
1841
|
if (payload.visibility === "private" && !grant.metadata?.["sessionId"]) {
|
|
1547
1842
|
if (!authorization) {
|
|
@@ -1549,39 +1844,13 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1549
1844
|
message: "managed human session required",
|
|
1550
1845
|
});
|
|
1551
1846
|
}
|
|
1552
|
-
await requireManagedHumanPrivateSessionCreate(
|
|
1847
|
+
await requireManagedHumanPrivateSessionCreate(unresolvedDeps, authorization, workspaceId);
|
|
1553
1848
|
if (payload.sandbox === "shared" || typeof payload.sandbox === "object") {
|
|
1554
1849
|
throw new HTTPException(422, {
|
|
1555
1850
|
message: "Only-me sessions require their own sandbox",
|
|
1556
1851
|
});
|
|
1557
1852
|
}
|
|
1558
1853
|
}
|
|
1559
|
-
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
1560
|
-
throw new HTTPException(422, {
|
|
1561
|
-
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
1562
|
-
});
|
|
1563
|
-
}
|
|
1564
|
-
// A committed keyed denial is the idempotent outcome even if mutable
|
|
1565
|
-
// resources, policy, authorization, or budget have changed since the first
|
|
1566
|
-
// attempt. Replay it before any of those checks, just as a keyed successful
|
|
1567
|
-
// session is returned rather than recreated later in createAndStartSession.
|
|
1568
|
-
if (payload.idempotencyKey) {
|
|
1569
|
-
const denial = await getSessionSpawnDenialByIdempotencyKey(
|
|
1570
|
-
db,
|
|
1571
|
-
workspaceId,
|
|
1572
|
-
payload.idempotencyKey,
|
|
1573
|
-
);
|
|
1574
|
-
if (denial) {
|
|
1575
|
-
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
await requireAtomicPersonalResourceAttachment(
|
|
1579
|
-
deps,
|
|
1580
|
-
authorization,
|
|
1581
|
-
workspaceId,
|
|
1582
|
-
payload.personalResourceAttachment,
|
|
1583
|
-
false,
|
|
1584
|
-
);
|
|
1585
1854
|
// Parent linkage and execution-context inheritance come ONLY from the
|
|
1586
1855
|
// worker-signed sessionId claim. A caller cannot nominate a parent in the
|
|
1587
1856
|
// payload, so inheriting an existing repository/tool/credential snapshot does
|
|
@@ -1592,7 +1861,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1592
1861
|
: null;
|
|
1593
1862
|
if (parentSessionId) {
|
|
1594
1863
|
try {
|
|
1595
|
-
await requireSessionAuthorization(
|
|
1864
|
+
await requireSessionAuthorization(unresolvedDeps, grant, {
|
|
1596
1865
|
sessionId: parentSessionId,
|
|
1597
1866
|
operation: "session.child.create",
|
|
1598
1867
|
surface: "core",
|
|
@@ -1650,6 +1919,118 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1650
1919
|
message: "caller attempt does not belong to the parent session",
|
|
1651
1920
|
});
|
|
1652
1921
|
}
|
|
1922
|
+
const replayManagedHumanSubjectId = creationInitiator.actor
|
|
1923
|
+
? (parentCallingTurn?.initiatingHumanSubjectId ??
|
|
1924
|
+
(parentCallingTurn?.initiator.kind === "subject"
|
|
1925
|
+
? parentCallingTurn.initiator.subjectId
|
|
1926
|
+
: null))
|
|
1927
|
+
: authorization?.canonicalManagedHumanSession || grant.principalKind === "human_session"
|
|
1928
|
+
? grant.subjectId
|
|
1929
|
+
: null;
|
|
1930
|
+
let retainedKeyedShellModel: string | null = null;
|
|
1931
|
+
if (
|
|
1932
|
+
payload.idempotencyKey &&
|
|
1933
|
+
(effectiveVisibility !== "user_private" || replayManagedHumanSubjectId !== null)
|
|
1934
|
+
) {
|
|
1935
|
+
try {
|
|
1936
|
+
const initializedReplay = await getInitializedSessionCreateReplay(db, {
|
|
1937
|
+
accountId: grant.accountId,
|
|
1938
|
+
workspaceId,
|
|
1939
|
+
subjectId: replayManagedHumanSubjectId ?? grant.subjectId,
|
|
1940
|
+
...(replayManagedHumanSubjectId
|
|
1941
|
+
? { activeManagedHumanSubjectId: replayManagedHumanSubjectId }
|
|
1942
|
+
: {}),
|
|
1943
|
+
createIdempotencyKey: payload.idempotencyKey,
|
|
1944
|
+
selectedInstalledSkillIds: payload.installedSkillIds ?? [],
|
|
1945
|
+
...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
|
|
1946
|
+
visibility: effectiveVisibility,
|
|
1947
|
+
variableSetIds: payload.variableSetIds ?? [],
|
|
1948
|
+
initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
|
|
1949
|
+
deferInitialTurn: payload.startMode === "realtime",
|
|
1950
|
+
});
|
|
1951
|
+
if (initializedReplay) {
|
|
1952
|
+
if (initializedReplay.outcome === "denied") {
|
|
1953
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(initializedReplay.denial));
|
|
1954
|
+
}
|
|
1955
|
+
if (initializedReplay.outcome === "pending") {
|
|
1956
|
+
retainedKeyedShellModel = initializedReplay.session.model;
|
|
1957
|
+
} else {
|
|
1958
|
+
if (initializedReplay.workflowWakeRevision !== null) {
|
|
1959
|
+
await unresolvedDeps.workflowClient.wakeSessionWorkflow({
|
|
1960
|
+
accountId: grant.accountId,
|
|
1961
|
+
workspaceId,
|
|
1962
|
+
sessionId: initializedReplay.session.id,
|
|
1963
|
+
workflowId: initializedReplay.temporalWorkflowId,
|
|
1964
|
+
wakeRevision: initializedReplay.workflowWakeRevision,
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
return await withSessionCreateUsageRecording({
|
|
1968
|
+
deps: unresolvedDeps,
|
|
1969
|
+
grant,
|
|
1970
|
+
workspaceId,
|
|
1971
|
+
startMode: payload.startMode,
|
|
1972
|
+
origin: creationInitiator.actor ? "system" : "user",
|
|
1973
|
+
createOutcome: {
|
|
1974
|
+
session: initializedReplay.session,
|
|
1975
|
+
outcome: initializedReplay.changed ? "repaired" : "replayed",
|
|
1976
|
+
replay: !initializedReplay.changed,
|
|
1977
|
+
changed: initializedReplay.changed,
|
|
1978
|
+
},
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
if (error instanceof SessionIdConflictError) {
|
|
1984
|
+
throw new HTTPException(409, {
|
|
1985
|
+
message: "requested session id is already in use",
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
if (error instanceof SessionCreateIdempotencyConflictError) {
|
|
1989
|
+
throw new HTTPException(409, { message: error.message, cause: error });
|
|
1990
|
+
}
|
|
1991
|
+
throw error;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
let settings = await resolveWorkspaceModelBoundarySettings(
|
|
1995
|
+
unresolvedDeps,
|
|
1996
|
+
grant,
|
|
1997
|
+
workspaceId,
|
|
1998
|
+
[payload.model],
|
|
1999
|
+
retainedKeyedShellModel,
|
|
2000
|
+
);
|
|
2001
|
+
let deps =
|
|
2002
|
+
settings === unresolvedDeps.settings
|
|
2003
|
+
? unresolvedDeps
|
|
2004
|
+
: {
|
|
2005
|
+
...unresolvedDeps,
|
|
2006
|
+
catalogSourceSettings: unresolvedDeps.catalogSourceSettings ?? unresolvedDeps.settings,
|
|
2007
|
+
settings,
|
|
2008
|
+
};
|
|
2009
|
+
const { bus, workflowClient, objectStorage } = deps;
|
|
2010
|
+
await requireAtomicPersonalResourceAttachment(
|
|
2011
|
+
deps,
|
|
2012
|
+
authorization,
|
|
2013
|
+
workspaceId,
|
|
2014
|
+
payload.personalResourceAttachment,
|
|
2015
|
+
false,
|
|
2016
|
+
);
|
|
2017
|
+
const inheritedModel = parentCallingTurn?.model ?? parentSession?.model ?? settings.openaiModel;
|
|
2018
|
+
const effectiveModelId = payload.model ?? inheritedModel;
|
|
2019
|
+
const effectiveCatalogSettings = await resolveWorkspaceModelBoundarySettings(
|
|
2020
|
+
deps,
|
|
2021
|
+
grant,
|
|
2022
|
+
workspaceId,
|
|
2023
|
+
[effectiveModelId],
|
|
2024
|
+
parentSession ? inheritedModel : null,
|
|
2025
|
+
);
|
|
2026
|
+
if (effectiveCatalogSettings !== settings) {
|
|
2027
|
+
deps = {
|
|
2028
|
+
...deps,
|
|
2029
|
+
catalogSourceSettings: deps.catalogSourceSettings ?? settings,
|
|
2030
|
+
settings: effectiveCatalogSettings,
|
|
2031
|
+
};
|
|
2032
|
+
settings = effectiveCatalogSettings;
|
|
2033
|
+
}
|
|
1653
2034
|
let effectiveGoal = payload.goal;
|
|
1654
2035
|
if (parentSession && payload.goal) {
|
|
1655
2036
|
try {
|
|
@@ -1723,9 +2104,43 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1723
2104
|
? payload.resources
|
|
1724
2105
|
: (parentSession?.resources ?? payload.resources),
|
|
1725
2106
|
);
|
|
1726
|
-
const
|
|
2107
|
+
const inheritedOrSubmittedSkills = hasOwnProperty(rawPayload, "skills")
|
|
1727
2108
|
? payload.skills
|
|
1728
2109
|
: (parentSession?.skills ?? payload.skills);
|
|
2110
|
+
const selectedInstalledSkillIds = payload.installedSkillIds ?? [];
|
|
2111
|
+
const selectedInstalledSkills: SessionSkill[] = [];
|
|
2112
|
+
if (selectedInstalledSkillIds.length > 0) {
|
|
2113
|
+
const installedSkills = await listInstalledPortableSkills(db, workspaceId, {
|
|
2114
|
+
includeSessionSelected: true,
|
|
2115
|
+
});
|
|
2116
|
+
const installedById = new Map(installedSkills.map((skill) => [skill.capabilityId, skill]));
|
|
2117
|
+
for (const capabilityId of selectedInstalledSkillIds) {
|
|
2118
|
+
const installed = installedById.get(capabilityId);
|
|
2119
|
+
if (!installed) {
|
|
2120
|
+
throw new HTTPException(422, {
|
|
2121
|
+
message: `Session-selected Skill is not installed in this workspace: ${capabilityId}`,
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
if (installed.activationMode !== "session_selected") {
|
|
2125
|
+
throw new HTTPException(422, {
|
|
2126
|
+
message: `Installed Skill does not require explicit session selection: ${capabilityId}`,
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
selectedInstalledSkills.push({
|
|
2130
|
+
name: installed.name,
|
|
2131
|
+
description: installed.description,
|
|
2132
|
+
files: installed.files.map((file) => ({ path: file.path, content: file.content })),
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
let skills: SessionSkill[];
|
|
2137
|
+
try {
|
|
2138
|
+
skills = SessionSkills.parse([...inheritedOrSubmittedSkills, ...selectedInstalledSkills]);
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
throw new HTTPException(422, {
|
|
2141
|
+
message: error instanceof Error ? error.message : "invalid session Skill selection",
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
1729
2144
|
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
1730
2145
|
// Visibility became durable draft state after older clients had already
|
|
1731
2146
|
// written rows without it. Compare it only when the create request supplied
|
|
@@ -1860,8 +2275,7 @@ export async function createSessionForRequestWithOutcome(
|
|
|
1860
2275
|
// managers: falling back to the deployment model would silently move a child
|
|
1861
2276
|
// onto the OpenGeni-credits billing path. Legacy session-bound grants without
|
|
1862
2277
|
// exact attempt claims fall back to the parent session's persisted defaults.
|
|
1863
|
-
const
|
|
1864
|
-
const model = canonicalConfiguredModel(settings, payload.model ?? inheritedModel);
|
|
2278
|
+
const model = canonicalConfiguredModel(settings, effectiveModelId);
|
|
1865
2279
|
if (model === null || model === undefined) {
|
|
1866
2280
|
throw new Error("effective session model unexpectedly resolved to null");
|
|
1867
2281
|
}
|
|
@@ -2025,7 +2439,10 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2025
2439
|
payload.firstPartyMcpTools,
|
|
2026
2440
|
parentSession ? parentSession.firstPartyMcpTools : undefined,
|
|
2027
2441
|
workspaceFirstPartyDefaults && !parentSession
|
|
2028
|
-
? {
|
|
2442
|
+
? {
|
|
2443
|
+
...deploymentFirstPartyMcpToolPolicy,
|
|
2444
|
+
default: workspaceFirstPartyDefaults,
|
|
2445
|
+
}
|
|
2029
2446
|
: deploymentFirstPartyMcpToolPolicy,
|
|
2030
2447
|
);
|
|
2031
2448
|
const googleDrivePublicationEnabled =
|
|
@@ -2101,7 +2518,10 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2101
2518
|
let sandboxGroupId: string | null = null;
|
|
2102
2519
|
let inheritedBackend: Session["sandboxBackend"] | undefined;
|
|
2103
2520
|
let inheritedSandboxOs: Session["sandboxOs"] | undefined;
|
|
2104
|
-
let inheritedActiveTarget: {
|
|
2521
|
+
let inheritedActiveTarget: {
|
|
2522
|
+
sandboxId: string;
|
|
2523
|
+
workingDir: string | null;
|
|
2524
|
+
} | null = null;
|
|
2105
2525
|
// ENV-AWARE GROUPING: under the CURRENT mechanics the workspace VariableSet is
|
|
2106
2526
|
// creation-time box state — the box's manifest env is fixed when it is cold-
|
|
2107
2527
|
// created, and the SDK's provided-session guard rejects any manifest-env delta
|
|
@@ -2405,9 +2825,12 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2405
2825
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
2406
2826
|
personalConnectionDelegations,
|
|
2407
2827
|
initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
|
|
2828
|
+
workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
|
|
2829
|
+
retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
|
|
2408
2830
|
...(xaiProviderAccountAuthoritySnapshot ? { xaiProviderAccountAuthoritySnapshot } : {}),
|
|
2409
2831
|
parentSessionId,
|
|
2410
2832
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
2833
|
+
selectedInstalledSkillIds,
|
|
2411
2834
|
maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
|
|
2412
2835
|
allowNestedAgentDepthIncrease: hasPermission(grant.permissions, "workspace:admin"),
|
|
2413
2836
|
subjectId: grant.subjectId,
|
|
@@ -2474,30 +2897,14 @@ export async function createSessionForRequestWithOutcome(
|
|
|
2474
2897
|
}
|
|
2475
2898
|
throw error;
|
|
2476
2899
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
quantity: 1,
|
|
2486
|
-
unit: "run",
|
|
2487
|
-
sourceResourceType: "session",
|
|
2488
|
-
sourceResourceId: createOutcome.session.id,
|
|
2489
|
-
sessionId: createOutcome.session.id,
|
|
2490
|
-
initiator: createOutcome.session.createdBy,
|
|
2491
|
-
initiatorContext: createOutcome.session.createdByContext,
|
|
2492
|
-
origin: creationInitiator.actor ? "system" : "user",
|
|
2493
|
-
idempotencyKey: `agent_run.created:${workspaceId}:${createOutcome.session.id}`,
|
|
2494
|
-
});
|
|
2495
|
-
} catch (error) {
|
|
2496
|
-
usageRecording = "failed";
|
|
2497
|
-
reportSessionUsageRecordingFailure(error);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
return { ...createOutcome, usageRecording };
|
|
2900
|
+
return await withSessionCreateUsageRecording({
|
|
2901
|
+
deps,
|
|
2902
|
+
grant,
|
|
2903
|
+
workspaceId,
|
|
2904
|
+
startMode: payload.startMode,
|
|
2905
|
+
origin: creationInitiator.actor ? "system" : "user",
|
|
2906
|
+
createOutcome,
|
|
2907
|
+
});
|
|
2501
2908
|
}
|
|
2502
2909
|
|
|
2503
2910
|
/** @internal Fixed public projection; the committed session outcome remains authoritative. */
|
|
@@ -2525,6 +2932,53 @@ export async function createSessionForRequest(
|
|
|
2525
2932
|
).session;
|
|
2526
2933
|
}
|
|
2527
2934
|
|
|
2935
|
+
function sessionPromptBoundaryRequestHash(input: {
|
|
2936
|
+
delivery: "send" | "steer";
|
|
2937
|
+
controlEtag: string | null;
|
|
2938
|
+
expectedDraftRevision: number | null;
|
|
2939
|
+
text: string;
|
|
2940
|
+
annotations: SubmittedTimelineAnnotation[];
|
|
2941
|
+
modelContext: string | null;
|
|
2942
|
+
resources: ResourceRef[];
|
|
2943
|
+
composerDraftResources?: ResourceRef[];
|
|
2944
|
+
model: string | null;
|
|
2945
|
+
reasoningEffort: ReasoningEffort | null;
|
|
2946
|
+
latencyMode: "standard" | "priority" | "fast" | null;
|
|
2947
|
+
source: "user" | "api";
|
|
2948
|
+
mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
|
|
2949
|
+
connectionAuthorities?: McpConnectionAuthoritySelection[];
|
|
2950
|
+
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
2951
|
+
commandActor: SessionCommandActor;
|
|
2952
|
+
}): string {
|
|
2953
|
+
return `prompt-boundary-v1:${canonicalSessionCommandHash({
|
|
2954
|
+
delivery: input.delivery,
|
|
2955
|
+
controlEtag: input.controlEtag,
|
|
2956
|
+
expectedDraftRevision: input.expectedDraftRevision,
|
|
2957
|
+
text: input.text,
|
|
2958
|
+
annotations: input.annotations,
|
|
2959
|
+
modelContext: input.modelContext,
|
|
2960
|
+
resources: input.resources,
|
|
2961
|
+
composerDraftResourcesProvided: input.composerDraftResources !== undefined,
|
|
2962
|
+
composerDraftResources: input.composerDraftResources ?? [],
|
|
2963
|
+
model: input.model,
|
|
2964
|
+
reasoningEffort: input.reasoningEffort,
|
|
2965
|
+
latencyMode: input.latencyMode,
|
|
2966
|
+
source: input.source,
|
|
2967
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates,
|
|
2968
|
+
connectionAuthorities: input.connectionAuthorities ?? [],
|
|
2969
|
+
personalResourceAttachment: input.personalResourceAttachment ?? null,
|
|
2970
|
+
...(input.commandActor.type === "service"
|
|
2971
|
+
? {
|
|
2972
|
+
serviceInitiator: {
|
|
2973
|
+
subjectId: input.commandActor.subjectId,
|
|
2974
|
+
subjectLabel: input.commandActor.subjectLabel ?? null,
|
|
2975
|
+
context: input.commandActor.context ?? {},
|
|
2976
|
+
},
|
|
2977
|
+
}
|
|
2978
|
+
: {}),
|
|
2979
|
+
})}`;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2528
2982
|
/**
|
|
2529
2983
|
* Full accept-user-message flow shared by the `user.message` branch of
|
|
2530
2984
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
@@ -2566,192 +3020,316 @@ export async function acceptSessionUserMessageWithOutcome(
|
|
|
2566
3020
|
interruptionCount: number;
|
|
2567
3021
|
replay: boolean;
|
|
2568
3022
|
}> {
|
|
2569
|
-
const {
|
|
3023
|
+
const { db, bus, workflowClient, objectStorage } = deps;
|
|
2570
3024
|
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
3025
|
+
const delivery = input.delivery ?? "send";
|
|
3026
|
+
const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
|
|
3027
|
+
const commandActor: SessionCommandActor = delegatedServiceInitiator
|
|
3028
|
+
? {
|
|
3029
|
+
type: "service",
|
|
3030
|
+
subjectId: delegatedServiceInitiator.initiator.subjectId,
|
|
3031
|
+
...(delegatedServiceInitiator.initiator.label
|
|
3032
|
+
? { subjectLabel: delegatedServiceInitiator.initiator.label }
|
|
3033
|
+
: {}),
|
|
3034
|
+
context: delegatedServiceInitiator.context,
|
|
3035
|
+
}
|
|
3036
|
+
: { type: "human", subjectId: grant.subjectId };
|
|
2571
3037
|
await requireSessionAuthorization(deps, grant, {
|
|
2572
3038
|
sessionId,
|
|
2573
|
-
operation:
|
|
3039
|
+
operation: delivery === "steer" ? "session.steer" : "session.append",
|
|
2574
3040
|
surface: "core",
|
|
2575
3041
|
});
|
|
2576
|
-
await requireAtomicPersonalResourceAttachment(
|
|
2577
|
-
deps,
|
|
2578
|
-
input.authorization,
|
|
2579
|
-
workspaceId,
|
|
2580
|
-
input.personalResourceAttachment,
|
|
2581
|
-
true,
|
|
2582
|
-
);
|
|
2583
|
-
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
2584
|
-
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
2585
|
-
// pure read with no side effects.
|
|
2586
|
-
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
2587
|
-
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
2588
|
-
const effectiveModel =
|
|
2589
|
-
canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
2590
|
-
if (effectiveModel === null) {
|
|
2591
|
-
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
2592
|
-
}
|
|
2593
|
-
try {
|
|
2594
|
-
assertSessionAllowsProductModel(existingSession, effectiveModel);
|
|
2595
|
-
} catch (error) {
|
|
2596
|
-
if (error instanceof CodexCompactionV2ProviderLockedError) {
|
|
2597
|
-
throw new HTTPException(422, { message: error.message, cause: error });
|
|
2598
|
-
}
|
|
2599
|
-
throw error;
|
|
2600
|
-
}
|
|
2601
|
-
const sessionReasoningEffort = existingSession.reasoningEffort;
|
|
2602
|
-
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
2603
|
-
const sessionLatencyMode = existingSession.latencyMode;
|
|
2604
|
-
const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
|
|
2605
|
-
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
2606
|
-
modelId: effectiveModel,
|
|
2607
|
-
requestedModelId: input.model ?? null,
|
|
2608
|
-
modelSource: input.model == null ? "session" : "explicit",
|
|
2609
|
-
reasoningEffort: effectiveReasoningEffort,
|
|
2610
|
-
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
2611
|
-
latencyMode: effectiveLatencyMode,
|
|
2612
|
-
latencyModeSource: input.latencyMode == null ? "session" : "explicit",
|
|
2613
|
-
});
|
|
2614
3042
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
2615
3043
|
const composerDraftResources = input.composerDraftResources
|
|
2616
3044
|
? normalizeResources(input.composerDraftResources)
|
|
2617
3045
|
: undefined;
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
3046
|
+
const boundaryRequestHash = input.clientEventId
|
|
3047
|
+
? sessionPromptBoundaryRequestHash({
|
|
3048
|
+
delivery,
|
|
3049
|
+
controlEtag: input.controlEtag ?? null,
|
|
3050
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
3051
|
+
text: input.text,
|
|
3052
|
+
annotations: input.annotations ?? [],
|
|
3053
|
+
modelContext: input.modelContext ?? null,
|
|
3054
|
+
resources: requestedResources,
|
|
3055
|
+
...(composerDraftResources ? { composerDraftResources } : {}),
|
|
3056
|
+
model: input.model ?? null,
|
|
3057
|
+
reasoningEffort: input.reasoningEffort ?? null,
|
|
3058
|
+
latencyMode: input.latencyMode ?? null,
|
|
3059
|
+
source,
|
|
3060
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
3061
|
+
...(input.connectionAuthorities
|
|
3062
|
+
? { connectionAuthorities: input.connectionAuthorities }
|
|
3063
|
+
: {}),
|
|
3064
|
+
...(input.personalResourceAttachment
|
|
3065
|
+
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
3066
|
+
: {}),
|
|
3067
|
+
commandActor,
|
|
3068
|
+
})
|
|
3069
|
+
: null;
|
|
3070
|
+
if (input.clientEventId && boundaryRequestHash) {
|
|
3071
|
+
const replay = await withWorkspaceSubjectSessionActivityRls(
|
|
3072
|
+
db,
|
|
3073
|
+
workspaceId,
|
|
3074
|
+
grant.subjectId,
|
|
3075
|
+
async (scopedDb) =>
|
|
3076
|
+
await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
|
|
3077
|
+
workspaceId,
|
|
3078
|
+
sessionId,
|
|
3079
|
+
subjectId: grant.subjectId,
|
|
3080
|
+
actor: commandActor,
|
|
3081
|
+
operationKey: input.clientEventId!,
|
|
3082
|
+
delivery,
|
|
3083
|
+
boundaryRequestHash,
|
|
3084
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
3085
|
+
}),
|
|
2622
3086
|
);
|
|
2623
|
-
if (
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
3087
|
+
if (replay) {
|
|
3088
|
+
return finalizePostUserMessageTurn(
|
|
3089
|
+
{
|
|
3090
|
+
db,
|
|
3091
|
+
bus,
|
|
3092
|
+
workflowClient,
|
|
3093
|
+
accountId: grant.accountId,
|
|
3094
|
+
workspaceId,
|
|
3095
|
+
sessionId,
|
|
3096
|
+
delivery,
|
|
3097
|
+
...(deps.schedulePromptPostCommit
|
|
3098
|
+
? { schedulePostCommit: deps.schedulePromptPostCommit }
|
|
3099
|
+
: {}),
|
|
3100
|
+
},
|
|
3101
|
+
replay,
|
|
3102
|
+
);
|
|
2627
3103
|
}
|
|
2628
3104
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
3105
|
+
try {
|
|
3106
|
+
await requireAtomicPersonalResourceAttachment(
|
|
3107
|
+
deps,
|
|
3108
|
+
input.authorization,
|
|
3109
|
+
workspaceId,
|
|
3110
|
+
input.personalResourceAttachment,
|
|
3111
|
+
true,
|
|
3112
|
+
);
|
|
3113
|
+
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
3114
|
+
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
3115
|
+
// pure read with no side effects.
|
|
3116
|
+
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
3117
|
+
const settings = await resolveWorkspaceModelBoundarySettings(
|
|
3118
|
+
deps,
|
|
3119
|
+
grant,
|
|
3120
|
+
workspaceId,
|
|
3121
|
+
[input.model ?? existingSession.model],
|
|
3122
|
+
existingSession.model,
|
|
3123
|
+
);
|
|
3124
|
+
if (settings !== deps.settings) {
|
|
3125
|
+
deps = {
|
|
3126
|
+
...deps,
|
|
3127
|
+
catalogSourceSettings: deps.catalogSourceSettings ?? deps.settings,
|
|
3128
|
+
settings,
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
3132
|
+
const effectiveModel =
|
|
3133
|
+
canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
3134
|
+
if (effectiveModel === null) {
|
|
3135
|
+
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
3136
|
+
}
|
|
3137
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
3138
|
+
try {
|
|
3139
|
+
assertSessionAllowsProductModel(existingSession, effectiveModel);
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
if (error instanceof CodexCompactionV2ProviderLockedError) {
|
|
3142
|
+
throw new HTTPException(422, { message: error.message, cause: error });
|
|
3143
|
+
}
|
|
3144
|
+
throw error;
|
|
3145
|
+
}
|
|
3146
|
+
const sessionReasoningEffort = existingSession.reasoningEffort;
|
|
3147
|
+
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
3148
|
+
const sessionLatencyMode = existingSession.latencyMode;
|
|
3149
|
+
const effectiveLatencyMode = input.latencyMode ?? sessionLatencyMode;
|
|
3150
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
3151
|
+
modelId: effectiveModel,
|
|
3152
|
+
requestedModelId: input.model ?? null,
|
|
3153
|
+
modelSource: input.model == null ? "session" : "explicit",
|
|
3154
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
3155
|
+
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
3156
|
+
latencyMode: effectiveLatencyMode,
|
|
3157
|
+
latencyModeSource: input.latencyMode == null ? "session" : "explicit",
|
|
2645
3158
|
});
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
2659
|
-
settings,
|
|
2660
|
-
grant,
|
|
2661
|
-
session: existingSession,
|
|
2662
|
-
updates: input.mcpCredentialUpdates ?? [],
|
|
2663
|
-
});
|
|
2664
|
-
const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
|
|
2665
|
-
const inheritedPersonalConnectionDelegations =
|
|
2666
|
-
connectionDelegationSource.kind === "turn"
|
|
2667
|
-
? await getSessionTurnPersonalConnectionDelegations(
|
|
2668
|
-
db,
|
|
2669
|
-
workspaceId,
|
|
2670
|
-
connectionDelegationSource.sessionId,
|
|
2671
|
-
connectionDelegationSource.turnId,
|
|
2672
|
-
)
|
|
2673
|
-
: null;
|
|
2674
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
2675
|
-
db,
|
|
2676
|
-
workspaceId,
|
|
2677
|
-
settings,
|
|
2678
|
-
inheritedPersonalConnectionDelegations
|
|
2679
|
-
? {
|
|
2680
|
-
personalConnectionDelegations: inheritedPersonalConnectionDelegations,
|
|
2681
|
-
}
|
|
2682
|
-
: { subjectId: grant.subjectId },
|
|
2683
|
-
);
|
|
2684
|
-
const personalConnectionDelegations = await freezePersonalConnectionDelegations({
|
|
2685
|
-
db,
|
|
2686
|
-
workspaceId,
|
|
2687
|
-
settings: runtimeSettings,
|
|
2688
|
-
tools: existingSession.tools,
|
|
2689
|
-
resources: [...existingSession.resources, ...requestedResources],
|
|
2690
|
-
source: connectionDelegationSource,
|
|
2691
|
-
targetSessionId: sessionId,
|
|
2692
|
-
googleDrivePublicationEnabled:
|
|
2693
|
-
existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
|
|
2694
|
-
existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
|
|
2695
|
-
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
2696
|
-
(existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
|
|
2697
|
-
existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
|
|
2698
|
-
atlassianEnabled:
|
|
2699
|
-
existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
2700
|
-
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
2701
|
-
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
2702
|
-
...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
|
|
2703
|
-
});
|
|
2704
|
-
const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
|
|
2705
|
-
await postUserMessageTurn({
|
|
3159
|
+
if (composerDraftResources) {
|
|
3160
|
+
const acceptedResources = new Set(requestedResources.map((resource) => stableJson(resource)));
|
|
3161
|
+
const unacceptedDraftResource = composerDraftResources.find(
|
|
3162
|
+
(resource) => !acceptedResources.has(stableJson(resource)),
|
|
3163
|
+
);
|
|
3164
|
+
if (unacceptedDraftResource) {
|
|
3165
|
+
throw new HTTPException(422, {
|
|
3166
|
+
message: "composer draft resources must be included in the accepted resource set",
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
const annotations = await validateSubmittedTimelineAnnotations(
|
|
2706
3171
|
db,
|
|
2707
|
-
bus,
|
|
2708
|
-
workflowClient,
|
|
2709
|
-
settings,
|
|
2710
|
-
accountId: grant.accountId,
|
|
2711
3172
|
workspaceId,
|
|
2712
3173
|
sessionId,
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3174
|
+
input.annotations ?? [],
|
|
3175
|
+
);
|
|
3176
|
+
await requireLimit(deps, {
|
|
3177
|
+
accountId: grant.accountId,
|
|
3178
|
+
workspaceId,
|
|
3179
|
+
action: "agent_run:create",
|
|
3180
|
+
quantity: 1,
|
|
3181
|
+
model: effectiveModel,
|
|
3182
|
+
});
|
|
3183
|
+
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
3184
|
+
throw new HTTPException(503, {
|
|
3185
|
+
message: "object storage is not configured",
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
await validateFileResources(
|
|
3189
|
+
db,
|
|
3190
|
+
grant.accountId,
|
|
3191
|
+
workspaceId,
|
|
3192
|
+
grant.subjectId,
|
|
3193
|
+
requestedResources,
|
|
3194
|
+
);
|
|
3195
|
+
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3196
|
+
...existingSession.resources,
|
|
3197
|
+
...requestedResources,
|
|
3198
|
+
]);
|
|
3199
|
+
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
3200
|
+
settings,
|
|
3201
|
+
grant,
|
|
3202
|
+
session: existingSession,
|
|
3203
|
+
updates: input.mcpCredentialUpdates ?? [],
|
|
3204
|
+
});
|
|
3205
|
+
const connectionDelegationSource = personalConnectionDelegationSourceForGrant(grant);
|
|
3206
|
+
const inheritedPersonalConnectionDelegations =
|
|
3207
|
+
connectionDelegationSource.kind === "turn"
|
|
3208
|
+
? await getSessionTurnPersonalConnectionDelegations(
|
|
3209
|
+
db,
|
|
3210
|
+
workspaceId,
|
|
3211
|
+
connectionDelegationSource.sessionId,
|
|
3212
|
+
connectionDelegationSource.turnId,
|
|
3213
|
+
)
|
|
3214
|
+
: null;
|
|
3215
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3216
|
+
db,
|
|
3217
|
+
workspaceId,
|
|
3218
|
+
settings,
|
|
3219
|
+
inheritedPersonalConnectionDelegations
|
|
2733
3220
|
? {
|
|
2734
|
-
|
|
2735
|
-
type: "service" as const,
|
|
2736
|
-
subjectId: delegatedServiceInitiator.initiator.subjectId,
|
|
2737
|
-
...(delegatedServiceInitiator.initiator.label
|
|
2738
|
-
? { subjectLabel: delegatedServiceInitiator.initiator.label }
|
|
2739
|
-
: {}),
|
|
2740
|
-
context: delegatedServiceInitiator.context,
|
|
2741
|
-
},
|
|
3221
|
+
personalConnectionDelegations: inheritedPersonalConnectionDelegations,
|
|
2742
3222
|
}
|
|
2743
|
-
: {}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
...
|
|
2751
|
-
|
|
2752
|
-
|
|
3223
|
+
: { subjectId: grant.subjectId },
|
|
3224
|
+
);
|
|
3225
|
+
const personalConnectionDelegations = await freezePersonalConnectionDelegations({
|
|
3226
|
+
db,
|
|
3227
|
+
workspaceId,
|
|
3228
|
+
settings: runtimeSettings,
|
|
3229
|
+
tools: existingSession.tools,
|
|
3230
|
+
resources: [...existingSession.resources, ...requestedResources],
|
|
3231
|
+
source: connectionDelegationSource,
|
|
3232
|
+
targetSessionId: sessionId,
|
|
3233
|
+
googleDrivePublicationEnabled:
|
|
3234
|
+
existingSession.firstPartyMcpTools.includes("editable_artifact_export") &&
|
|
3235
|
+
existingSession.firstPartyMcpTools.includes("editable_artifact_export_status") &&
|
|
3236
|
+
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
3237
|
+
(existingSession.firstPartyMcpPermissions.includes("artifacts:read") &&
|
|
3238
|
+
existingSession.firstPartyMcpPermissions.includes("artifacts:publish"))),
|
|
3239
|
+
atlassianEnabled:
|
|
3240
|
+
existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
3241
|
+
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
3242
|
+
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
3243
|
+
...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
|
|
2753
3244
|
});
|
|
2754
|
-
|
|
3245
|
+
const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
|
|
3246
|
+
await postUserMessageTurn({
|
|
3247
|
+
db,
|
|
3248
|
+
bus,
|
|
3249
|
+
workflowClient,
|
|
3250
|
+
settings,
|
|
3251
|
+
accountId: grant.accountId,
|
|
3252
|
+
workspaceId,
|
|
3253
|
+
sessionId,
|
|
3254
|
+
text: input.text,
|
|
3255
|
+
annotations,
|
|
3256
|
+
modelContext: input.modelContext ?? null,
|
|
3257
|
+
resources: requestedResources,
|
|
3258
|
+
...(composerDraftResources ? { composerDraftResources } : {}),
|
|
3259
|
+
model: input.model ?? null,
|
|
3260
|
+
reasoningEffort: input.reasoningEffort ?? null,
|
|
3261
|
+
latencyMode: input.latencyMode ?? null,
|
|
3262
|
+
reasoningEffortFallback: sessionReasoningEffort,
|
|
3263
|
+
turnExecutionPolicy,
|
|
3264
|
+
mcpCredentialUpdates,
|
|
3265
|
+
personalConnectionDelegations,
|
|
3266
|
+
...(input.personalResourceAttachment
|
|
3267
|
+
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
3268
|
+
: {}),
|
|
3269
|
+
delivery,
|
|
3270
|
+
origin: source === "api" ? "operator" : "human",
|
|
3271
|
+
actor: grant.subjectId,
|
|
3272
|
+
...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
|
|
3273
|
+
commandActor,
|
|
3274
|
+
...(boundaryRequestHash ? { boundaryRequestHash } : {}),
|
|
3275
|
+
...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
|
|
3276
|
+
...(input.expectedDraftRevision !== undefined
|
|
3277
|
+
? { expectedDraftRevision: input.expectedDraftRevision }
|
|
3278
|
+
: {}),
|
|
3279
|
+
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
3280
|
+
recordAgentRunUsage: true,
|
|
3281
|
+
...(deps.schedulePromptPostCommit
|
|
3282
|
+
? { schedulePostCommit: deps.schedulePromptPostCommit }
|
|
3283
|
+
: {}),
|
|
3284
|
+
});
|
|
3285
|
+
return {
|
|
3286
|
+
accepted,
|
|
3287
|
+
turn,
|
|
3288
|
+
draft,
|
|
3289
|
+
receipt,
|
|
3290
|
+
routing,
|
|
3291
|
+
interruptionCount,
|
|
3292
|
+
replay,
|
|
3293
|
+
};
|
|
3294
|
+
} catch (error) {
|
|
3295
|
+
if (input.clientEventId && boundaryRequestHash) {
|
|
3296
|
+
const replay = await withWorkspaceSubjectSessionActivityRls(
|
|
3297
|
+
db,
|
|
3298
|
+
workspaceId,
|
|
3299
|
+
grant.subjectId,
|
|
3300
|
+
async (scopedDb) =>
|
|
3301
|
+
await replaySubmittedHumanPromptFromBoundaryReceipt(scopedDb, {
|
|
3302
|
+
workspaceId,
|
|
3303
|
+
sessionId,
|
|
3304
|
+
subjectId: grant.subjectId,
|
|
3305
|
+
actor: commandActor,
|
|
3306
|
+
operationKey: input.clientEventId!,
|
|
3307
|
+
delivery,
|
|
3308
|
+
boundaryRequestHash,
|
|
3309
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
3310
|
+
serializeOperation: true,
|
|
3311
|
+
}),
|
|
3312
|
+
);
|
|
3313
|
+
if (replay) {
|
|
3314
|
+
return finalizePostUserMessageTurn(
|
|
3315
|
+
{
|
|
3316
|
+
db,
|
|
3317
|
+
bus,
|
|
3318
|
+
workflowClient,
|
|
3319
|
+
accountId: grant.accountId,
|
|
3320
|
+
workspaceId,
|
|
3321
|
+
sessionId,
|
|
3322
|
+
delivery,
|
|
3323
|
+
...(deps.schedulePromptPostCommit
|
|
3324
|
+
? { schedulePostCommit: deps.schedulePromptPostCommit }
|
|
3325
|
+
: {}),
|
|
3326
|
+
},
|
|
3327
|
+
replay,
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
throw error;
|
|
3332
|
+
}
|
|
2755
3333
|
}
|
|
2756
3334
|
|
|
2757
3335
|
/** Backward-compatible entity-returning path used by existing REST callers. */
|