@opengeni/core 3.0.1 → 4.0.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/README.md +1 -1
- package/dist/application/connect-operation.d.ts +3 -0
- package/dist/application/sandbox-recovery.d.ts +49 -0
- package/dist/{chunk-H7X52RI2.js → chunk-TFHSE7XR.js} +13 -2
- package/dist/chunk-TFHSE7XR.js.map +1 -0
- package/dist/dependencies.d.ts +1 -1
- package/dist/domain/capabilities.d.ts +2 -2
- package/dist/domain/connection-account-selection-error.d.ts +6 -0
- package/dist/domain/connector-tool-permissions.d.ts +27 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +8 -0
- package/dist/domain/environments.d.ts +3 -7
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights.d.ts +4 -1
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +9 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +19 -10
- package/dist/domain/pr-review.d.ts +56 -4
- package/dist/domain/scheduled-tasks.d.ts +8 -5
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/sessions.d.ts +8 -6
- package/dist/domain/skills.d.ts +29 -1
- package/dist/editable-artifacts.js +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4868 -4803
- package/dist/index.js.map +1 -1
- package/dist/sandbox/routing.d.ts +2 -1
- package/dist/sandbox/runtime-settings.d.ts +1 -15
- package/package.json +11 -19
- package/src/application/composer-submit.ts +2 -1
- package/src/application/connect-operation.ts +32 -3
- package/src/application/sandbox-recovery.ts +55 -0
- package/src/application/user-resource-grants.ts +1 -16
- package/src/dependencies.ts +5 -1
- package/src/domain/capabilities.ts +169 -122
- package/src/domain/connection-account-selection-error.ts +9 -0
- package/src/domain/connector-tool-permissions.ts +305 -0
- package/src/domain/editable-artifacts/agent-application.ts +31 -2
- package/src/domain/environments.ts +4 -9
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights.ts +59 -35
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +251 -0
- package/src/domain/knowledge-preparation.ts +126 -0
- package/src/domain/mcp-account-bindings.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +83 -0
- package/src/domain/personal-connection-delegations.ts +226 -177
- package/src/domain/pr-review.ts +22 -6
- package/src/domain/resources.ts +9 -2
- package/src/domain/scheduled-tasks.ts +200 -155
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-tool-policy.ts +12 -4
- package/src/domain/sessions.ts +390 -261
- package/src/domain/skills.ts +9 -0
- package/src/index.ts +10 -6
- package/src/rigs/index.ts +17 -17
- package/src/sandbox/routing.ts +18 -7
- package/src/sandbox/runtime-settings.ts +7 -91
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
package/src/domain/sessions.ts
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
+
import { acceptSessionFileAttachments } from "@opengeni/db";
|
|
1
2
|
import { knowledgeContextForAccess } from "./knowledge";
|
|
3
|
+
import {
|
|
4
|
+
getSessionEvent,
|
|
5
|
+
getSessionRetryReceiptInTransaction,
|
|
6
|
+
retryFailedSessionInTransaction,
|
|
7
|
+
SessionRetryConflictError,
|
|
8
|
+
} from "@opengeni/db";
|
|
9
|
+
import type { SessionRetryRequest, SessionRetryResponse } from "@opengeni/contracts";
|
|
2
10
|
import { saveAgentLearningSettings } from "@opengeni/db";
|
|
3
11
|
import { withSessionRlsActorContext } from "@opengeni/db";
|
|
4
12
|
import { fileOwnerContextForAccess, fileOwnerContextForAgent } from "./file-owner";
|
|
5
13
|
import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
|
|
6
14
|
import { sessionCreationMetadata } from "../site-session-origin";
|
|
7
|
-
|
|
8
|
-
hostMcpBindingMatchesSelection,
|
|
9
|
-
HostMcpCreateSelections,
|
|
10
|
-
type HostMcpCreateSelection,
|
|
11
|
-
} from "@opengeni/contracts/host-mcp-bindings";
|
|
12
|
-
import {
|
|
13
|
-
captureDirectHostMcpAuthority,
|
|
14
|
-
HostMcpDelegationAuthorityError,
|
|
15
|
-
HostMcpBindingConflictError,
|
|
16
|
-
getHostMcpBinding,
|
|
17
|
-
getHostMcpDelegation,
|
|
18
|
-
} from "@opengeni/db";
|
|
15
|
+
|
|
19
16
|
import {
|
|
20
17
|
canonicalizeConfiguredModelId,
|
|
21
18
|
configuredAllowedModels,
|
|
@@ -55,7 +52,8 @@ import {
|
|
|
55
52
|
type GoalSpec,
|
|
56
53
|
type FirstPartyMcpToolName,
|
|
57
54
|
type McpPersonalConnectionDelegation,
|
|
58
|
-
type
|
|
55
|
+
type McpConnectionAccountBinding,
|
|
56
|
+
type McpConnectionAccountSelection,
|
|
59
57
|
type Permission,
|
|
60
58
|
type PersonalResourceAttachmentIntent,
|
|
61
59
|
type ReasoningEffort,
|
|
@@ -71,6 +69,7 @@ import {
|
|
|
71
69
|
type SessionMcpCredentialUpdateInput,
|
|
72
70
|
type SessionMcpServerInput,
|
|
73
71
|
type SessionMcpServerMetadata,
|
|
72
|
+
type SessionMcpApprovalPolicyTarget,
|
|
74
73
|
type SubmittedTimelineAnnotation,
|
|
75
74
|
type TimelineAnnotation,
|
|
76
75
|
type UpdateSessionMcpApprovalPolicyResponse,
|
|
@@ -101,6 +100,8 @@ import {
|
|
|
101
100
|
listDistinctVariableSetSelectionsInGroup,
|
|
102
101
|
listDistinctRigVersionIdsInGroup,
|
|
103
102
|
listInstalledPortableSkills,
|
|
103
|
+
listEnabledMcpCapabilityServers,
|
|
104
|
+
requireApprovalWithFloor,
|
|
104
105
|
getSandbox,
|
|
105
106
|
getSession,
|
|
106
107
|
getInitializedSessionCreateReplay,
|
|
@@ -153,7 +154,7 @@ import { hasPermission, requirePermission, type AccessGrantAuthorization } from
|
|
|
153
154
|
import { externalCreationMetadata } from "./external-creation-attribution";
|
|
154
155
|
import { prepareExternalLinkTurnAdmission } from "../application/external-link-work-admission";
|
|
155
156
|
import { externalContinuationCommitAuthorizer } from "../application/external-continuation";
|
|
156
|
-
|
|
157
|
+
|
|
157
158
|
import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
|
|
158
159
|
import type {
|
|
159
160
|
AcceptSessionUserMessageDependencies,
|
|
@@ -166,7 +167,7 @@ import {
|
|
|
166
167
|
requireSessionAuthorization,
|
|
167
168
|
SessionAuthorizationDeniedError,
|
|
168
169
|
} from "../session-authorization";
|
|
169
|
-
import {
|
|
170
|
+
import { assertNativeMcpConnectionRef } from "./native-mcp-connection-admission";
|
|
170
171
|
import {
|
|
171
172
|
preflightCreateTimeSandboxTarget,
|
|
172
173
|
swapActiveSandbox,
|
|
@@ -183,7 +184,7 @@ import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
|
183
184
|
import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
|
|
184
185
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
185
186
|
import {
|
|
186
|
-
|
|
187
|
+
freezeConnectionAccounts,
|
|
187
188
|
personalConnectionDelegationSourceForGrant,
|
|
188
189
|
} from "./personal-connection-delegations";
|
|
189
190
|
import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
|
|
@@ -209,6 +210,26 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
|
209
210
|
// the generic lossy projection cannot silently rewrite this audit fact.
|
|
210
211
|
const maxToolPolicyAuditRefs = 40;
|
|
211
212
|
|
|
213
|
+
function withoutExcludedMcpServers(
|
|
214
|
+
tools: ToolRef[],
|
|
215
|
+
excludedIds: readonly string[] = [],
|
|
216
|
+
): ToolRef[] {
|
|
217
|
+
const excluded = new Set(excludedIds);
|
|
218
|
+
return tools.filter((tool) => tool.id === "opengeni" || !excluded.has(tool.id));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function defaultPolicyExclusions(
|
|
222
|
+
ids: readonly string[] = [],
|
|
223
|
+
): Pick<SessionToolPolicy, "excludedMcpServerIds"> {
|
|
224
|
+
const sorted = [...new Set(ids)].filter((id) => id !== "opengeni").sort();
|
|
225
|
+
if (sorted.length > 64 || sorted.some((id) => id.length > 200 || !/^[A-Za-z0-9_-]+$/.test(id))) {
|
|
226
|
+
throw new HTTPException(422, {
|
|
227
|
+
message: "connector exclusions must contain at most 64 valid MCP server IDs",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return sorted.length ? { excludedMcpServerIds: sorted } : {};
|
|
231
|
+
}
|
|
232
|
+
|
|
212
233
|
function isCatalogOverlayModel(modelId: string | null | undefined): boolean {
|
|
213
234
|
return (
|
|
214
235
|
modelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
|
|
@@ -566,7 +587,7 @@ function validateSessionMcpServersForCreate(
|
|
|
566
587
|
const dbServers: CreateSessionMcpServerInput[] = [];
|
|
567
588
|
const metadata: SessionMcpServerMetadata[] = [];
|
|
568
589
|
for (const server of servers) {
|
|
569
|
-
|
|
590
|
+
assertNativeMcpConnectionRef(server.connectionRef);
|
|
570
591
|
if (seenIds.has(server.id)) {
|
|
571
592
|
throw new HTTPException(422, {
|
|
572
593
|
message: `duplicate session MCP server id: ${server.id}`,
|
|
@@ -760,7 +781,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
760
781
|
captureInitialTurnAuthority?: (tx: Database, sessionId: string, turnId: string) => Promise<void>;
|
|
761
782
|
/** Internal replay identity; the verified caller must also capture authority.
|
|
762
783
|
* Supplying selection metadata alone never grants runtime use. */
|
|
763
|
-
|
|
784
|
+
|
|
764
785
|
/** The custom workspace model was frozen by an earlier accepted boundary or
|
|
765
786
|
* inherited from an existing session, so retirement must not invalidate it. */
|
|
766
787
|
retainWorkspaceGatewayModel?: boolean;
|
|
@@ -838,8 +859,10 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
838
859
|
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
839
860
|
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
840
861
|
mcpServers?: CreateSessionMcpServerInput[];
|
|
862
|
+
mcpApprovalPolicies?: Record<string, SessionMcpApprovalPolicy>;
|
|
841
863
|
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
842
864
|
personalConnectionDelegations?: McpPersonalConnectionDelegation[];
|
|
865
|
+
mcpAccountBindings?: McpConnectionAccountBinding[] | null;
|
|
843
866
|
initialPersonalResourceAttachmentIntent?: PersonalResourceAttachmentIntent | null;
|
|
844
867
|
xaiProviderAccountAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1;
|
|
845
868
|
// The manager session spawning this worker (a worker-signed sessionId claim
|
|
@@ -1034,7 +1057,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1034
1057
|
toolPolicy: input.toolPolicy,
|
|
1035
1058
|
metadata: sessionMetadata,
|
|
1036
1059
|
initialAgentLearning: input.initialAgentLearning,
|
|
1037
|
-
|
|
1060
|
+
|
|
1038
1061
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
1039
1062
|
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
1040
1063
|
createdByActor: input.createdByActor ?? null,
|
|
@@ -1060,7 +1083,9 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1060
1083
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
1061
1084
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
1062
1085
|
mcpServers: input.mcpServers ?? [],
|
|
1086
|
+
mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
|
|
1063
1087
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
1088
|
+
mcpAccountBindings: input.mcpAccountBindings ?? null,
|
|
1064
1089
|
initialPersonalResourceAttachmentIntent:
|
|
1065
1090
|
input.initialPersonalResourceAttachmentIntent ?? null,
|
|
1066
1091
|
...(input.xaiProviderAccountAuthoritySnapshot
|
|
@@ -1131,7 +1156,7 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1131
1156
|
metadata: sessionMetadata,
|
|
1132
1157
|
initialAgentLearning: input.initialAgentLearning,
|
|
1133
1158
|
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
1134
|
-
|
|
1159
|
+
|
|
1135
1160
|
...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
|
|
1136
1161
|
createdByActor: input.createdByActor ?? null,
|
|
1137
1162
|
model: input.model,
|
|
@@ -1154,7 +1179,9 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1154
1179
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
1155
1180
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
1156
1181
|
mcpServers: input.mcpServers ?? [],
|
|
1182
|
+
mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
|
|
1157
1183
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
1184
|
+
mcpAccountBindings: input.mcpAccountBindings ?? null,
|
|
1158
1185
|
initialPersonalResourceAttachmentIntent:
|
|
1159
1186
|
input.initialPersonalResourceAttachmentIntent ?? null,
|
|
1160
1187
|
...(input.xaiProviderAccountAuthoritySnapshot
|
|
@@ -1325,6 +1352,9 @@ async function finishStartSession(
|
|
|
1325
1352
|
...(input.goal.rootConstraints !== undefined
|
|
1326
1353
|
? { rootConstraints: input.goal.rootConstraints }
|
|
1327
1354
|
: {}),
|
|
1355
|
+
...(input.goal.reportRequirements !== undefined
|
|
1356
|
+
? { reportRequirements: input.goal.reportRequirements }
|
|
1357
|
+
: {}),
|
|
1328
1358
|
...(input.goal.maxAutoContinuations !== undefined
|
|
1329
1359
|
? { maxAutoContinuations: input.goal.maxAutoContinuations }
|
|
1330
1360
|
: {}),
|
|
@@ -1547,7 +1577,8 @@ type PostUserMessageTurnInput = {
|
|
|
1547
1577
|
clientEventId?: string;
|
|
1548
1578
|
mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
|
|
1549
1579
|
personalConnectionDelegations?: McpPersonalConnectionDelegation[];
|
|
1550
|
-
|
|
1580
|
+
mcpAccountBindings?: McpConnectionAccountBinding[] | null;
|
|
1581
|
+
|
|
1551
1582
|
captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
|
|
1552
1583
|
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
1553
1584
|
delivery?: "send" | "steer";
|
|
@@ -1755,9 +1786,8 @@ export async function postUserMessageTurn(
|
|
|
1755
1786
|
? { recordAgentRunUsage: input.recordAgentRunUsage }
|
|
1756
1787
|
: {}),
|
|
1757
1788
|
personalConnectionDelegations: input.personalConnectionDelegations ?? [],
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
: {}),
|
|
1789
|
+
mcpAccountBindings: input.mcpAccountBindings ?? null,
|
|
1790
|
+
|
|
1761
1791
|
...(input.captureTurnAuthority
|
|
1762
1792
|
? { captureTurnAuthority: input.captureTurnAuthority }
|
|
1763
1793
|
: {}),
|
|
@@ -1981,6 +2011,116 @@ async function resolveWorkspaceModelBoundarySettings(
|
|
|
1981
2011
|
).settings;
|
|
1982
2012
|
}
|
|
1983
2013
|
|
|
2014
|
+
/** Explicit recovery, never a new prompt or a Pause/Resume command. */
|
|
2015
|
+
export async function retryFailedSession(
|
|
2016
|
+
deps: ApiRouteDeps,
|
|
2017
|
+
grant: AccessGrant,
|
|
2018
|
+
workspaceId: string,
|
|
2019
|
+
sessionId: string,
|
|
2020
|
+
request: SessionRetryRequest,
|
|
2021
|
+
): Promise<SessionRetryResponse> {
|
|
2022
|
+
requirePermission(grant, "sessions:control");
|
|
2023
|
+
await requireSessionAuthorization(deps, grant, {
|
|
2024
|
+
sessionId,
|
|
2025
|
+
operation: "session.control",
|
|
2026
|
+
surface: "core",
|
|
2027
|
+
});
|
|
2028
|
+
const replay = await withWorkspaceSubjectSessionActivityRls(
|
|
2029
|
+
deps.db,
|
|
2030
|
+
workspaceId,
|
|
2031
|
+
grant.subjectId,
|
|
2032
|
+
async (db) =>
|
|
2033
|
+
await getSessionRetryReceiptInTransaction(db, {
|
|
2034
|
+
workspaceId,
|
|
2035
|
+
sessionId,
|
|
2036
|
+
subjectId: grant.subjectId,
|
|
2037
|
+
request,
|
|
2038
|
+
}),
|
|
2039
|
+
);
|
|
2040
|
+
if (replay) return replay;
|
|
2041
|
+
const session = await requireSession(deps.db, workspaceId, sessionId);
|
|
2042
|
+
const failure = await getSessionEvent(deps.db, workspaceId, request.failureEventId);
|
|
2043
|
+
if (!failure || failure.sessionId !== sessionId)
|
|
2044
|
+
throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "The failure event is unavailable");
|
|
2045
|
+
const turn = failure?.turnId ? await getSessionTurn(deps.db, workspaceId, failure.turnId) : null;
|
|
2046
|
+
const retainedModel = turn?.model ?? session.model;
|
|
2047
|
+
const settings = await resolveWorkspaceModelBoundarySettings(
|
|
2048
|
+
deps,
|
|
2049
|
+
grant,
|
|
2050
|
+
workspaceId,
|
|
2051
|
+
[request.model ?? retainedModel],
|
|
2052
|
+
retainedModel,
|
|
2053
|
+
);
|
|
2054
|
+
const model = canonicalConfiguredModel(settings, request.model ?? retainedModel)!;
|
|
2055
|
+
try {
|
|
2056
|
+
assertSessionAllowsProductModel(session, model);
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
if (error instanceof CodexCompactionV2ProviderLockedError)
|
|
2059
|
+
throw new HTTPException(422, { message: error.message, cause: error });
|
|
2060
|
+
throw error;
|
|
2061
|
+
}
|
|
2062
|
+
await assertWorkspaceModelPolicyAllows(deps.db, settings, workspaceId, model);
|
|
2063
|
+
const executionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
2064
|
+
modelId: model,
|
|
2065
|
+
requestedModelId: request.model ?? null,
|
|
2066
|
+
modelSource: request.model === undefined ? "session" : "explicit",
|
|
2067
|
+
reasoningEffort: request.reasoningEffort ?? turn?.reasoningEffort ?? session.reasoningEffort,
|
|
2068
|
+
reasoningSource: request.reasoningEffort === undefined ? "session" : "explicit",
|
|
2069
|
+
latencyMode: request.latencyMode ?? turn?.latencyMode ?? session.latencyMode,
|
|
2070
|
+
latencyModeSource: request.latencyMode === undefined ? "session" : "explicit",
|
|
2071
|
+
});
|
|
2072
|
+
await requireLimit(deps, {
|
|
2073
|
+
accountId: grant.accountId,
|
|
2074
|
+
workspaceId,
|
|
2075
|
+
action: "agent_run:create",
|
|
2076
|
+
quantity: 1,
|
|
2077
|
+
model,
|
|
2078
|
+
});
|
|
2079
|
+
const result = await runIdempotentPersistenceTransaction(
|
|
2080
|
+
{
|
|
2081
|
+
stage: "session.retry",
|
|
2082
|
+
eventTypes: ["turn.recovery.requested", "session.status.changed"],
|
|
2083
|
+
maxAttempts: 3,
|
|
2084
|
+
},
|
|
2085
|
+
async () =>
|
|
2086
|
+
await withWorkspaceSubjectSessionActivityRls(
|
|
2087
|
+
deps.db,
|
|
2088
|
+
workspaceId,
|
|
2089
|
+
grant.subjectId,
|
|
2090
|
+
async (db) =>
|
|
2091
|
+
await retryFailedSessionInTransaction(db, {
|
|
2092
|
+
accountId: grant.accountId,
|
|
2093
|
+
workspaceId,
|
|
2094
|
+
sessionId,
|
|
2095
|
+
subjectId: grant.subjectId,
|
|
2096
|
+
request,
|
|
2097
|
+
executionPolicy,
|
|
2098
|
+
}),
|
|
2099
|
+
),
|
|
2100
|
+
);
|
|
2101
|
+
// Fanout and workflow dispatch are reconstructible after the durable commit.
|
|
2102
|
+
// Never report an admitted retry as failed because one notification is down.
|
|
2103
|
+
void Promise.all([
|
|
2104
|
+
deps.workflowClient.requestSessionWorkflowWakeDispatch(),
|
|
2105
|
+
(async () => {
|
|
2106
|
+
const events = await Promise.all(
|
|
2107
|
+
result.eventIds.map((id) => getSessionEvent(deps.db, workspaceId, id)),
|
|
2108
|
+
);
|
|
2109
|
+
await publishDurableSessionEvents(
|
|
2110
|
+
deps.bus,
|
|
2111
|
+
workspaceId,
|
|
2112
|
+
sessionId,
|
|
2113
|
+
events.filter((event): event is SessionEvent => event !== null),
|
|
2114
|
+
);
|
|
2115
|
+
})(),
|
|
2116
|
+
]).catch(() => {
|
|
2117
|
+
console.warn("[sessions] retry notification failed; durable recovery remains", {
|
|
2118
|
+
errorCode: "session_retry_notification_failed",
|
|
2119
|
+
});
|
|
2120
|
+
});
|
|
2121
|
+
return { outcome: result.outcome, turnId: result.turnId, failureEventId: result.failureEventId };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
1984
2124
|
async function withSessionCreateUsageRecording(input: {
|
|
1985
2125
|
deps: ApiRouteDeps;
|
|
1986
2126
|
grant: AccessGrant;
|
|
@@ -2027,24 +2167,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2027
2167
|
payload.metadata = sessionCreationMetadata(payload.metadata);
|
|
2028
2168
|
const creationMetadata = externalCreationMetadata(payload.metadata, authorization, grant);
|
|
2029
2169
|
const externalBeforeCreateCommit = externalContinuationCommitAuthorizer(authorization);
|
|
2030
|
-
|
|
2031
|
-
if (
|
|
2032
|
-
hostSelections.length &&
|
|
2033
|
-
(!authorization ||
|
|
2034
|
-
authorization.grant.accountId !== grant.accountId ||
|
|
2035
|
-
authorization.grant.subjectId !== grant.subjectId ||
|
|
2036
|
-
authorization?.grant.workspaceId !== workspaceId ||
|
|
2037
|
-
!hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
|
|
2038
|
-
!hasPermission(grant.permissions, "connections:read") ||
|
|
2039
|
-
grant.metadata?.["sessionId"] ||
|
|
2040
|
-
payload.startMode === "realtime")
|
|
2041
|
-
) {
|
|
2042
|
-
throw new HTTPException(403, {
|
|
2043
|
-
message: "Host selection requires a verified direct owner and non-realtime start",
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
if (hostSelections.length)
|
|
2047
|
-
prepareHostMcpOwnerAuthorization(authorization!, workspaceId, "connections:read");
|
|
2170
|
+
|
|
2048
2171
|
if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
|
|
2049
2172
|
throw new HTTPException(422, {
|
|
2050
2173
|
message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
|
|
@@ -2193,7 +2316,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2193
2316
|
selectedInstalledSkillIds: payload.installedSkillIds ?? [],
|
|
2194
2317
|
initialAgentLearning: payload.agentLearning,
|
|
2195
2318
|
...sessionScope,
|
|
2196
|
-
|
|
2319
|
+
|
|
2197
2320
|
...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
|
|
2198
2321
|
visibility: effectiveVisibility,
|
|
2199
2322
|
variableSetIds: payload.variableSetIds ?? [],
|
|
@@ -2351,6 +2474,23 @@ async function createSessionForRequestInFileScope(
|
|
|
2351
2474
|
capabilityRuntimeSettings,
|
|
2352
2475
|
sessionMcpServers.runtimeServers,
|
|
2353
2476
|
);
|
|
2477
|
+
const requestedMcpPolicies =
|
|
2478
|
+
payload.mcpApprovalPolicies ?? parentSession?.mcpApprovalPolicies ?? {};
|
|
2479
|
+
const mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy> = {};
|
|
2480
|
+
if (Object.keys(requestedMcpPolicies).length > 0) {
|
|
2481
|
+
requirePermission(grant, "sessions:control");
|
|
2482
|
+
const inheritedServers = await listEnabledMcpCapabilityServers(db, workspaceId);
|
|
2483
|
+
for (const [id, policy] of Object.entries(requestedMcpPolicies)) {
|
|
2484
|
+
const inherited = inheritedServers.find((server) => server.id === id);
|
|
2485
|
+
if (!inherited || sessionMcpServers.runtimeServers.some((server) => server.id === id)) {
|
|
2486
|
+
throw new HTTPException(422, {
|
|
2487
|
+
message: `MCP approval policy must name an enabled inherited capability: ${id}`,
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
mcpApprovalPolicies[id] =
|
|
2491
|
+
requireApprovalWithFloor(policy, inherited.approvalFloor, true) ?? false;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2354
2494
|
const resources = normalizeResources(
|
|
2355
2495
|
hasOwnProperty(rawPayload, "resources")
|
|
2356
2496
|
? payload.resources
|
|
@@ -2395,6 +2535,11 @@ async function createSessionForRequestInFileScope(
|
|
|
2395
2535
|
});
|
|
2396
2536
|
}
|
|
2397
2537
|
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
2538
|
+
if (toolsProvided && payload.excludedMcpServerIds !== undefined) {
|
|
2539
|
+
throw new HTTPException(422, {
|
|
2540
|
+
message: "connector exclusions require workspace-default tools",
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2398
2543
|
// Visibility became durable draft state after older clients had already
|
|
2399
2544
|
// written rows without it. Compare it only when the create request supplied
|
|
2400
2545
|
// the field explicitly; the parsed schema default must not manufacture a
|
|
@@ -2417,6 +2562,9 @@ async function createSessionForRequestInFileScope(
|
|
|
2417
2562
|
)
|
|
2418
2563
|
: parentSession.tools,
|
|
2419
2564
|
runtimeSettings,
|
|
2565
|
+
).filter(
|
|
2566
|
+
(tool) =>
|
|
2567
|
+
tool.id === "opengeni" || !parentSession.toolPolicy.excludedMcpServerIds?.includes(tool.id),
|
|
2420
2568
|
);
|
|
2421
2569
|
if (toolsProvided) {
|
|
2422
2570
|
assertToolRefsSubset(
|
|
@@ -2434,6 +2582,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2434
2582
|
toolPolicy = {
|
|
2435
2583
|
mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
|
|
2436
2584
|
inheritedFromSessionId: parentSession.id,
|
|
2585
|
+
...defaultPolicyExclusions(parentSession.toolPolicy.excludedMcpServerIds),
|
|
2437
2586
|
};
|
|
2438
2587
|
}
|
|
2439
2588
|
} else if (toolsProvided) {
|
|
@@ -2448,19 +2597,27 @@ async function createSessionForRequestInFileScope(
|
|
|
2448
2597
|
);
|
|
2449
2598
|
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
2450
2599
|
}
|
|
2600
|
+
if (payload.excludedMcpServerIds !== undefined) {
|
|
2601
|
+
if (toolPolicy.mode !== "workspace_default") {
|
|
2602
|
+
throw new HTTPException(403, {
|
|
2603
|
+
message: "connector exclusions require workspace-default tools",
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
toolPolicy = {
|
|
2607
|
+
...toolPolicy,
|
|
2608
|
+
...defaultPolicyExclusions([
|
|
2609
|
+
...(toolPolicy.excludedMcpServerIds ?? []),
|
|
2610
|
+
...payload.excludedMcpServerIds,
|
|
2611
|
+
]),
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
selectedTools = withoutExcludedMcpServers(selectedTools, toolPolicy.excludedMcpServerIds);
|
|
2451
2615
|
// The first-party MCP server is attached to EVERY session. Registration is
|
|
2452
2616
|
// independently intersected with the exact model-visible selection and the
|
|
2453
2617
|
// tool's permission/target authorization predicate, so attachment alone
|
|
2454
2618
|
// exposes nothing.
|
|
2455
2619
|
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
2456
|
-
|
|
2457
|
-
runtimeSettings,
|
|
2458
|
-
tools,
|
|
2459
|
-
grant,
|
|
2460
|
-
workspaceId,
|
|
2461
|
-
hostSelections,
|
|
2462
|
-
authorization,
|
|
2463
|
-
);
|
|
2620
|
+
|
|
2464
2621
|
const captureLinkedAuthority = prepareExternalLinkTurnAdmission(authorization);
|
|
2465
2622
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
2466
2623
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
@@ -2468,20 +2625,20 @@ async function createSessionForRequestInFileScope(
|
|
|
2468
2625
|
message: "object storage is not configured",
|
|
2469
2626
|
});
|
|
2470
2627
|
}
|
|
2628
|
+
const attachmentOwnerContext = authorization
|
|
2629
|
+
? await fileOwnerContextForAccess({ db }, authorization, "sessions:create")
|
|
2630
|
+
: grant.principalKind === "agent_attempt"
|
|
2631
|
+
? await fileOwnerContextForAgent({ db }, grant, "sessions:create")
|
|
2632
|
+
: undefined;
|
|
2633
|
+
const attachmentOwner =
|
|
2634
|
+
attachmentOwnerContext?.privateFileOwnerSubjectId === grant.subjectId ? grant.subjectId : null;
|
|
2471
2635
|
await validateFileResources(
|
|
2472
2636
|
db,
|
|
2473
2637
|
grant.accountId,
|
|
2474
2638
|
workspaceId,
|
|
2475
2639
|
personalResourceSubjectId ?? grant.subjectId,
|
|
2476
2640
|
resources,
|
|
2477
|
-
|
|
2478
|
-
(effectiveVisibility === "user_private" ||
|
|
2479
|
-
sessionScope.memoryScope === "user" ||
|
|
2480
|
-
workspace.kind === "personal")
|
|
2481
|
-
? authorization
|
|
2482
|
-
? await fileOwnerContextForAccess({ db }, authorization, "sessions:create")
|
|
2483
|
-
: await fileOwnerContextForAgent({ db }, grant, "sessions:create")
|
|
2484
|
-
: undefined,
|
|
2641
|
+
attachmentOwnerContext,
|
|
2485
2642
|
);
|
|
2486
2643
|
// Every selected Variable Set is independently authorized. Scope does not
|
|
2487
2644
|
// affect precedence: explicit order is low-to-high and later sets win name
|
|
@@ -2513,7 +2670,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2513
2670
|
if (payload.rigId) {
|
|
2514
2671
|
throw new HTTPException(422, {
|
|
2515
2672
|
message: rig
|
|
2516
|
-
? `
|
|
2673
|
+
? `sandbox environment ${payload.rigId} has no active version to bind`
|
|
2517
2674
|
: `unknown rigId: ${payload.rigId}`,
|
|
2518
2675
|
});
|
|
2519
2676
|
}
|
|
@@ -2584,6 +2741,9 @@ async function createSessionForRequestInFileScope(
|
|
|
2584
2741
|
reasoningEffort,
|
|
2585
2742
|
latencyMode,
|
|
2586
2743
|
options: {
|
|
2744
|
+
...(payload.excludedMcpServerIds !== undefined
|
|
2745
|
+
? { excludedMcpServerIds: payload.excludedMcpServerIds }
|
|
2746
|
+
: {}),
|
|
2587
2747
|
...(visibilityProvided ? { visibility: payload.visibility } : {}),
|
|
2588
2748
|
...(payload.sandboxBackend ? { sandboxBackend: payload.sandboxBackend } : {}),
|
|
2589
2749
|
...(payload.targetSandboxId ? { targetSandboxId: payload.targetSandboxId } : {}),
|
|
@@ -2730,14 +2890,15 @@ async function createSessionForRequestInFileScope(
|
|
|
2730
2890
|
const atlassianEnabled =
|
|
2731
2891
|
firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
2732
2892
|
(!firstPartyMcpPermissions?.length || firstPartyMcpPermissions.includes("connections:read"));
|
|
2733
|
-
const personalConnectionDelegations = await
|
|
2893
|
+
const { personalConnectionDelegations, mcpAccountBindings } = await freezeConnectionAccounts({
|
|
2734
2894
|
db,
|
|
2895
|
+
accountId: grant.accountId,
|
|
2735
2896
|
workspaceId,
|
|
2736
2897
|
settings: runtimeSettings,
|
|
2737
2898
|
tools,
|
|
2738
2899
|
resources,
|
|
2739
2900
|
source: connectionDelegationSource,
|
|
2740
|
-
authoritySelections: payload.
|
|
2901
|
+
authoritySelections: payload.connectionAccounts,
|
|
2741
2902
|
googleDrivePublicationEnabled,
|
|
2742
2903
|
atlassianEnabled,
|
|
2743
2904
|
});
|
|
@@ -2862,7 +3023,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2862
3023
|
throw new HTTPException(422, {
|
|
2863
3024
|
message: variableSetMismatch
|
|
2864
3025
|
? "sandbox:'shared' requires the same variableSet / same environment as the creator's box (the box variable set/environment is fixed at creation); omit sandbox or pass 'new' when attaching a different variableSet/environment."
|
|
2865
|
-
: "sandbox:'shared' requires the same
|
|
3026
|
+
: "sandbox:'shared' requires the same sandbox environment as the creator's box (setup is fixed at creation); omit sandbox or pass 'new' when selecting a different sandbox environment.",
|
|
2866
3027
|
});
|
|
2867
3028
|
}
|
|
2868
3029
|
// Inherited default: deterministic separation on the genuine shared-state
|
|
@@ -2925,7 +3086,7 @@ async function createSessionForRequestInFileScope(
|
|
|
2925
3086
|
)
|
|
2926
3087
|
) {
|
|
2927
3088
|
throw new HTTPException(422, {
|
|
2928
|
-
message: `sandbox group ${sandboxChoice.groupId}
|
|
3089
|
+
message: `sandbox group ${sandboxChoice.groupId} uses a different sandbox environment (setup is fixed at creation); select the group's sandbox environment or omit sandbox for a separate box.`,
|
|
2929
3090
|
});
|
|
2930
3091
|
}
|
|
2931
3092
|
}
|
|
@@ -3118,8 +3279,8 @@ async function createSessionForRequestInFileScope(
|
|
|
3118
3279
|
sandboxGroupId,
|
|
3119
3280
|
metadata: creationMetadata ?? {},
|
|
3120
3281
|
...(beforeCreateCommit ? { beforeCreateCommit } : {}),
|
|
3121
|
-
|
|
3122
|
-
...(
|
|
3282
|
+
|
|
3283
|
+
...(payload.startMode !== "realtime"
|
|
3123
3284
|
? {
|
|
3124
3285
|
captureInitialTurnAuthority: async (
|
|
3125
3286
|
tx: Database,
|
|
@@ -3127,7 +3288,15 @@ async function createSessionForRequestInFileScope(
|
|
|
3127
3288
|
turnId: string,
|
|
3128
3289
|
) => {
|
|
3129
3290
|
await captureLinkedAuthority?.(tx, sessionId, turnId);
|
|
3130
|
-
|
|
3291
|
+
if (attachmentOwner)
|
|
3292
|
+
await acceptSessionFileAttachments(tx, {
|
|
3293
|
+
accountId: grant.accountId,
|
|
3294
|
+
workspaceId,
|
|
3295
|
+
sessionId,
|
|
3296
|
+
turnId,
|
|
3297
|
+
subjectId: attachmentOwner,
|
|
3298
|
+
resources,
|
|
3299
|
+
});
|
|
3131
3300
|
},
|
|
3132
3301
|
}
|
|
3133
3302
|
: {}),
|
|
@@ -3156,8 +3325,10 @@ async function createSessionForRequestInFileScope(
|
|
|
3156
3325
|
firstPartyMcpPermissions,
|
|
3157
3326
|
firstPartyMcpTools,
|
|
3158
3327
|
mcpServers: sessionMcpServers.dbServers,
|
|
3328
|
+
mcpApprovalPolicies,
|
|
3159
3329
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
3160
3330
|
personalConnectionDelegations,
|
|
3331
|
+
mcpAccountBindings,
|
|
3161
3332
|
initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
|
|
3162
3333
|
workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
|
|
3163
3334
|
retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
|
|
@@ -3213,18 +3384,7 @@ async function createSessionForRequestInFileScope(
|
|
|
3213
3384
|
{ message: error.message, cause: error },
|
|
3214
3385
|
);
|
|
3215
3386
|
}
|
|
3216
|
-
|
|
3217
|
-
throw new HTTPException(403, {
|
|
3218
|
-
message: "Host delegation authority unavailable",
|
|
3219
|
-
cause: error,
|
|
3220
|
-
});
|
|
3221
|
-
}
|
|
3222
|
-
if (error instanceof HostMcpBindingConflictError) {
|
|
3223
|
-
throw new HTTPException(409, {
|
|
3224
|
-
message: "Host delegation selection conflicts",
|
|
3225
|
-
cause: error,
|
|
3226
|
-
});
|
|
3227
|
-
}
|
|
3387
|
+
|
|
3228
3388
|
if (error instanceof AgentCommandAuthorityError) {
|
|
3229
3389
|
throw new HTTPException(403, { message: error.message });
|
|
3230
3390
|
}
|
|
@@ -3279,89 +3439,6 @@ export async function createSessionForRequest(
|
|
|
3279
3439
|
).session;
|
|
3280
3440
|
}
|
|
3281
3441
|
|
|
3282
|
-
/** Snapshot the selected destination, then recheck live actor, binding and grant
|
|
3283
|
-
* inside the transaction which accepts the turn. Neither selection nor a
|
|
3284
|
-
* registered binding alone is execution authority. Shared by create/send/steer. */
|
|
3285
|
-
function prepareSelectedHostTurnAuthority(
|
|
3286
|
-
settings: Settings,
|
|
3287
|
-
tools: ToolRef[],
|
|
3288
|
-
grant: AccessGrant,
|
|
3289
|
-
workspaceId: string,
|
|
3290
|
-
selections: HostMcpCreateSelection[],
|
|
3291
|
-
authorization?: AccessGrantAuthorization,
|
|
3292
|
-
): ((tx: Database, sessionId: string, turnId: string) => Promise<void>) | undefined {
|
|
3293
|
-
if (!selections.length) return undefined;
|
|
3294
|
-
if (
|
|
3295
|
-
!authorization ||
|
|
3296
|
-
authorization.grant.accountId !== grant.accountId ||
|
|
3297
|
-
authorization.grant.subjectId !== grant.subjectId ||
|
|
3298
|
-
authorization?.grant.workspaceId !== workspaceId ||
|
|
3299
|
-
!hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
|
|
3300
|
-
!hasPermission(grant.permissions, "connections:read") ||
|
|
3301
|
-
grant.metadata?.["sessionId"]
|
|
3302
|
-
)
|
|
3303
|
-
throw new HTTPException(403, {
|
|
3304
|
-
message: "Host selection requires a verified direct owner",
|
|
3305
|
-
});
|
|
3306
|
-
const beforeCommit = prepareHostMcpOwnerAuthorization(
|
|
3307
|
-
authorization,
|
|
3308
|
-
workspaceId,
|
|
3309
|
-
"connections:read",
|
|
3310
|
-
);
|
|
3311
|
-
const configs = new Map(
|
|
3312
|
-
selections.map((selection) => {
|
|
3313
|
-
const configured = settings.mcpServers.find((server) => server.id === selection.serverId);
|
|
3314
|
-
if (
|
|
3315
|
-
!configured ||
|
|
3316
|
-
!tools.some((tool) => tool.kind === "mcp" && tool.id === selection.serverId) ||
|
|
3317
|
-
configured.connectionRef?.authoritySource !== "host" ||
|
|
3318
|
-
!configured.connectionRef.hostBinding ||
|
|
3319
|
-
!configured.url
|
|
3320
|
-
)
|
|
3321
|
-
throw new HTTPException(422, {
|
|
3322
|
-
message: "Host delegation must match a selected configured host server",
|
|
3323
|
-
});
|
|
3324
|
-
assertHostMcpAuthoritySourceAdmissionEnabled(settings, configured.connectionRef);
|
|
3325
|
-
return [selection.serverId, structuredClone(configured)] as const;
|
|
3326
|
-
}),
|
|
3327
|
-
);
|
|
3328
|
-
return async (tx, sessionId, turnId) => {
|
|
3329
|
-
const owner = await beforeCommit(tx);
|
|
3330
|
-
for (const selection of selections) {
|
|
3331
|
-
const delegation = await getHostMcpDelegation(tx, owner, selection.delegationId);
|
|
3332
|
-
const binding = delegation ? await getHostMcpBinding(tx, owner, delegation.bindingId) : null;
|
|
3333
|
-
const configured = configs.get(selection.serverId)!;
|
|
3334
|
-
if (
|
|
3335
|
-
!delegation ||
|
|
3336
|
-
!binding ||
|
|
3337
|
-
delegation.status !== "active" ||
|
|
3338
|
-
binding.status !== "active" ||
|
|
3339
|
-
delegation.generation !== selection.generation ||
|
|
3340
|
-
!hostMcpBindingMatchesSelection(binding, {
|
|
3341
|
-
serverId: selection.serverId,
|
|
3342
|
-
destinationUrl: configured.url!,
|
|
3343
|
-
connectionRef: configured.connectionRef!,
|
|
3344
|
-
})
|
|
3345
|
-
)
|
|
3346
|
-
throw new HTTPException(403, { message: "Host delegation selection changed" });
|
|
3347
|
-
try {
|
|
3348
|
-
await captureDirectHostMcpAuthority(tx, owner, {
|
|
3349
|
-
sessionId,
|
|
3350
|
-
turnId,
|
|
3351
|
-
delegationId: delegation.id,
|
|
3352
|
-
expectedDelegationGeneration: selection.generation,
|
|
3353
|
-
});
|
|
3354
|
-
} catch (error) {
|
|
3355
|
-
if (error instanceof HostMcpDelegationAuthorityError)
|
|
3356
|
-
throw new HTTPException(403, { message: "Host delegation authority unavailable" });
|
|
3357
|
-
if (error instanceof HostMcpBindingConflictError)
|
|
3358
|
-
throw new HTTPException(409, { message: "Host delegation selection conflicts" });
|
|
3359
|
-
throw error;
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
};
|
|
3363
|
-
}
|
|
3364
|
-
|
|
3365
3442
|
function sessionPromptBoundaryRequestHash(input: {
|
|
3366
3443
|
delivery: "send" | "steer";
|
|
3367
3444
|
controlEtag: string | null;
|
|
@@ -3376,8 +3453,7 @@ function sessionPromptBoundaryRequestHash(input: {
|
|
|
3376
3453
|
latencyMode: "standard" | "priority" | "fast" | null;
|
|
3377
3454
|
source: "user" | "api";
|
|
3378
3455
|
mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
|
|
3379
|
-
|
|
3380
|
-
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
3456
|
+
connectionAccounts?: McpConnectionAccountSelection[];
|
|
3381
3457
|
personalResourceAttachment?: PersonalResourceAttachmentIntent;
|
|
3382
3458
|
commandActor: SessionCommandActor;
|
|
3383
3459
|
}): string {
|
|
@@ -3396,10 +3472,7 @@ function sessionPromptBoundaryRequestHash(input: {
|
|
|
3396
3472
|
latencyMode: input.latencyMode,
|
|
3397
3473
|
source: input.source,
|
|
3398
3474
|
mcpCredentialUpdates: input.mcpCredentialUpdates,
|
|
3399
|
-
|
|
3400
|
-
...(input.selectedHostMcpDelegations?.length
|
|
3401
|
-
? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
|
|
3402
|
-
: {}),
|
|
3475
|
+
connectionAccounts: input.connectionAccounts ?? [],
|
|
3403
3476
|
personalResourceAttachment: input.personalResourceAttachment ?? null,
|
|
3404
3477
|
...(input.commandActor.type === "service"
|
|
3405
3478
|
? {
|
|
@@ -3437,8 +3510,7 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3437
3510
|
latencyMode?: "standard" | "priority" | "fast" | null;
|
|
3438
3511
|
clientEventId?: string;
|
|
3439
3512
|
mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
|
|
3440
|
-
|
|
3441
|
-
selectedHostMcpDelegations?: HostMcpCreateSelection[];
|
|
3513
|
+
connectionAccounts?: McpConnectionAccountSelection[];
|
|
3442
3514
|
delivery?: "send" | "steer";
|
|
3443
3515
|
origin?: "human" | "operator";
|
|
3444
3516
|
controlEtag?: string | null;
|
|
@@ -3456,24 +3528,7 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3456
3528
|
replay: boolean;
|
|
3457
3529
|
}> {
|
|
3458
3530
|
const { db, bus, workflowClient, objectStorage } = deps;
|
|
3459
|
-
|
|
3460
|
-
(a, b) => (a.serverId < b.serverId ? -1 : a.serverId > b.serverId ? 1 : 0),
|
|
3461
|
-
);
|
|
3462
|
-
if (
|
|
3463
|
-
hostSelections.length &&
|
|
3464
|
-
(!input.authorization ||
|
|
3465
|
-
input.authorization.grant.accountId !== grant.accountId ||
|
|
3466
|
-
input.authorization.grant.subjectId !== grant.subjectId ||
|
|
3467
|
-
input.authorization?.grant.workspaceId !== workspaceId ||
|
|
3468
|
-
!hasPermission(input.authorization?.grant.permissions ?? [], "connections:read") ||
|
|
3469
|
-
!hasPermission(grant.permissions, "connections:read") ||
|
|
3470
|
-
grant.metadata?.["sessionId"])
|
|
3471
|
-
)
|
|
3472
|
-
throw new HTTPException(403, {
|
|
3473
|
-
message: "Host selection requires a verified direct owner",
|
|
3474
|
-
});
|
|
3475
|
-
if (hostSelections.length)
|
|
3476
|
-
prepareHostMcpOwnerAuthorization(input.authorization!, workspaceId, "connections:read");
|
|
3531
|
+
|
|
3477
3532
|
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
3478
3533
|
const delivery = input.delivery ?? "send";
|
|
3479
3534
|
const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
|
|
@@ -3511,10 +3566,7 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3511
3566
|
latencyMode: input.latencyMode ?? null,
|
|
3512
3567
|
source,
|
|
3513
3568
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
3514
|
-
|
|
3515
|
-
...(input.connectionAuthorities
|
|
3516
|
-
? { connectionAuthorities: input.connectionAuthorities }
|
|
3517
|
-
: {}),
|
|
3569
|
+
...(input.connectionAccounts ? { connectionAccounts: input.connectionAccounts } : {}),
|
|
3518
3570
|
...(input.personalResourceAttachment
|
|
3519
3571
|
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
3520
3572
|
: {}),
|
|
@@ -3639,21 +3691,18 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3639
3691
|
message: "object storage is not configured",
|
|
3640
3692
|
});
|
|
3641
3693
|
}
|
|
3694
|
+
const attachmentOwnerContext = input.authorization
|
|
3695
|
+
? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
|
|
3696
|
+
: grant.principalKind === "agent_attempt"
|
|
3697
|
+
? await fileOwnerContextForAgent({ db }, grant, "sessions:control")
|
|
3698
|
+
: undefined;
|
|
3642
3699
|
await validateFileResources(
|
|
3643
3700
|
db,
|
|
3644
3701
|
grant.accountId,
|
|
3645
3702
|
workspaceId,
|
|
3646
3703
|
grant.subjectId,
|
|
3647
3704
|
requestedResources,
|
|
3648
|
-
|
|
3649
|
-
((await getSessionAuthorityProjection(db, workspaceId, sessionId))?.visibility ===
|
|
3650
|
-
"user_private" ||
|
|
3651
|
-
existingSession.memoryScope === "user" ||
|
|
3652
|
-
(await requireWorkspace(db, workspaceId)).kind === "personal")
|
|
3653
|
-
? input.authorization
|
|
3654
|
-
? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
|
|
3655
|
-
: await fileOwnerContextForAgent({ db }, grant, "sessions:control")
|
|
3656
|
-
: undefined,
|
|
3705
|
+
attachmentOwnerContext,
|
|
3657
3706
|
);
|
|
3658
3707
|
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3659
3708
|
...existingSession.resources,
|
|
@@ -3675,18 +3724,22 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3675
3724
|
connectionDelegationSource.turnId,
|
|
3676
3725
|
)
|
|
3677
3726
|
: null;
|
|
3678
|
-
const runtimeSettings =
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3727
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3728
|
+
await settingsWithEnabledCapabilityMcpServers(
|
|
3729
|
+
db,
|
|
3730
|
+
workspaceId,
|
|
3731
|
+
settings,
|
|
3732
|
+
inheritedPersonalConnectionDelegations
|
|
3733
|
+
? {
|
|
3734
|
+
personalConnectionDelegations: inheritedPersonalConnectionDelegations,
|
|
3735
|
+
}
|
|
3736
|
+
: { subjectId: grant.subjectId },
|
|
3737
|
+
),
|
|
3738
|
+
existingSession.mcpServers,
|
|
3687
3739
|
);
|
|
3688
|
-
const personalConnectionDelegations = await
|
|
3740
|
+
const { personalConnectionDelegations, mcpAccountBindings } = await freezeConnectionAccounts({
|
|
3689
3741
|
db,
|
|
3742
|
+
accountId: grant.accountId,
|
|
3690
3743
|
workspaceId,
|
|
3691
3744
|
settings: runtimeSettings,
|
|
3692
3745
|
tools: existingSession.tools,
|
|
@@ -3703,16 +3756,9 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3703
3756
|
existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
|
|
3704
3757
|
(!existingSession.firstPartyMcpPermissions?.length ||
|
|
3705
3758
|
existingSession.firstPartyMcpPermissions.includes("connections:read")),
|
|
3706
|
-
...(input.
|
|
3759
|
+
...(input.connectionAccounts ? { authoritySelections: input.connectionAccounts } : {}),
|
|
3707
3760
|
});
|
|
3708
|
-
|
|
3709
|
-
settingsWithSessionMcpServerMetadata(runtimeSettings, existingSession.mcpServers),
|
|
3710
|
-
existingSession.tools,
|
|
3711
|
-
grant,
|
|
3712
|
-
workspaceId,
|
|
3713
|
-
hostSelections,
|
|
3714
|
-
input.authorization,
|
|
3715
|
-
);
|
|
3761
|
+
|
|
3716
3762
|
const captureLinkedAuthority = prepareExternalLinkTurnAdmission(input.authorization);
|
|
3717
3763
|
const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
|
|
3718
3764
|
await postUserMessageTurn({
|
|
@@ -3735,13 +3781,12 @@ async function acceptSessionUserMessageInFileScope(
|
|
|
3735
3781
|
turnExecutionPolicy,
|
|
3736
3782
|
mcpCredentialUpdates,
|
|
3737
3783
|
personalConnectionDelegations,
|
|
3738
|
-
|
|
3739
|
-
|
|
3784
|
+
mcpAccountBindings,
|
|
3785
|
+
|
|
3786
|
+
...(captureLinkedAuthority
|
|
3740
3787
|
? {
|
|
3741
|
-
captureTurnAuthority:
|
|
3742
|
-
|
|
3743
|
-
await captureSelectedHostAuthority?.(tx, sessionId, turnId);
|
|
3744
|
-
},
|
|
3788
|
+
captureTurnAuthority: (tx: Database, turnId: string) =>
|
|
3789
|
+
captureLinkedAuthority(tx, sessionId, turnId),
|
|
3745
3790
|
}
|
|
3746
3791
|
: {}),
|
|
3747
3792
|
...(input.personalResourceAttachment
|
|
@@ -3905,7 +3950,7 @@ export async function updateSessionMcpApprovalPolicy(
|
|
|
3905
3950
|
});
|
|
3906
3951
|
requirePermission(grant, "sessions:control");
|
|
3907
3952
|
|
|
3908
|
-
const outcome: { server?:
|
|
3953
|
+
const outcome: { server?: SessionMcpApprovalPolicyTarget } = {};
|
|
3909
3954
|
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
3910
3955
|
deps.db,
|
|
3911
3956
|
grant.workspaceId,
|
|
@@ -3976,6 +4021,12 @@ function toolPolicyAuditSnapshot(
|
|
|
3976
4021
|
return {
|
|
3977
4022
|
mode: policy.mode,
|
|
3978
4023
|
inheritedFromSessionId: policy.inheritedFromSessionId,
|
|
4024
|
+
...(policy.excludedMcpServerIds?.length
|
|
4025
|
+
? {
|
|
4026
|
+
excludedMcpServerIds: policy.excludedMcpServerIds.slice(0, maxToolPolicyAuditRefs),
|
|
4027
|
+
excludedMcpServerCount: policy.excludedMcpServerIds.length,
|
|
4028
|
+
}
|
|
4029
|
+
: {}),
|
|
3979
4030
|
// IDs only: no MCP URLs, names, headers, credentials, schemas, or args.
|
|
3980
4031
|
toolIds: [...toolRefs]
|
|
3981
4032
|
.sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`))
|
|
@@ -3984,7 +4035,9 @@ function toolPolicyAuditSnapshot(
|
|
|
3984
4035
|
toolCount: allToolRefs.length,
|
|
3985
4036
|
firstPartyMcpTools: [...firstPartyMcpTools].sort(),
|
|
3986
4037
|
firstPartyMcpToolCount: firstPartyMcpTools.length,
|
|
3987
|
-
truncated:
|
|
4038
|
+
truncated:
|
|
4039
|
+
allToolRefs.length > toolRefs.length ||
|
|
4040
|
+
(policy.excludedMcpServerIds?.length ?? 0) > maxToolPolicyAuditRefs,
|
|
3988
4041
|
};
|
|
3989
4042
|
}
|
|
3990
4043
|
|
|
@@ -4028,9 +4081,31 @@ export async function updateSessionToolPolicy(
|
|
|
4028
4081
|
);
|
|
4029
4082
|
const explicitRequest = request.mode === "workspace_default" ? null : request;
|
|
4030
4083
|
const requestedMode = explicitRequest ? "explicit" : "workspace_default";
|
|
4084
|
+
const connectorOnlyEdit =
|
|
4085
|
+
request.mode === "workspace_default" && request.excludedMcpServerIds !== undefined;
|
|
4086
|
+
const requestedExclusions =
|
|
4087
|
+
request.mode === "workspace_default" ? (request.excludedMcpServerIds ?? []) : [];
|
|
4031
4088
|
const explicitRequestedTools = explicitRequest
|
|
4032
4089
|
? (() => {
|
|
4033
|
-
|
|
4090
|
+
// A disconnected stored selection remains policy truth, not executable
|
|
4091
|
+
// authority. Preserve exact existing refs while validating every new or
|
|
4092
|
+
// changed ref against the current runtime registry.
|
|
4093
|
+
const availableIds = new Set(runtimeSettings.mcpServers.map((server) => server.id));
|
|
4094
|
+
const retainedUnavailableRefs = explicitRequest.tools.filter(
|
|
4095
|
+
(tool) =>
|
|
4096
|
+
!availableIds.has(tool.id) &&
|
|
4097
|
+
existingSession.tools.some((existing) => stableJson(existing) === stableJson(tool)),
|
|
4098
|
+
);
|
|
4099
|
+
const retainedIds = new Set(retainedUnavailableRefs.map((tool) => tool.id));
|
|
4100
|
+
const validatedCurrentRefs = validateToolRefs(
|
|
4101
|
+
explicitRequest.tools.filter((tool) => !retainedIds.has(tool.id)),
|
|
4102
|
+
runtimeSettings,
|
|
4103
|
+
);
|
|
4104
|
+
const validatedTools = explicitRequest.tools.filter(
|
|
4105
|
+
(tool) =>
|
|
4106
|
+
retainedUnavailableRefs.includes(tool) ||
|
|
4107
|
+
validatedCurrentRefs.some((validated) => validated.id === tool.id),
|
|
4108
|
+
);
|
|
4034
4109
|
const validatedIds = new Set(validatedTools.map((tool) => `${tool.kind}:${tool.id}`));
|
|
4035
4110
|
const unknown = explicitRequest.tools.find(
|
|
4036
4111
|
(tool) => !validatedIds.has(`${tool.kind}:${tool.id}`),
|
|
@@ -4100,6 +4175,9 @@ export async function updateSessionToolPolicy(
|
|
|
4100
4175
|
)
|
|
4101
4176
|
: parent.tools,
|
|
4102
4177
|
runtimeSettings,
|
|
4178
|
+
).filter(
|
|
4179
|
+
(tool) =>
|
|
4180
|
+
tool.id === "opengeni" || !parent.toolPolicy.excludedMcpServerIds?.includes(tool.id),
|
|
4103
4181
|
);
|
|
4104
4182
|
const deploymentAllowedFirstPartyMcpTools = new Set(
|
|
4105
4183
|
deploymentFirstPartyMcpToolPolicy.allowed,
|
|
@@ -4119,6 +4197,10 @@ export async function updateSessionToolPolicy(
|
|
|
4119
4197
|
nextPolicy = {
|
|
4120
4198
|
mode: "workspace_default",
|
|
4121
4199
|
inheritedFromSessionId: parent.id,
|
|
4200
|
+
...defaultPolicyExclusions([
|
|
4201
|
+
...(parent.toolPolicy.excludedMcpServerIds ?? []),
|
|
4202
|
+
...requestedExclusions,
|
|
4203
|
+
]),
|
|
4122
4204
|
};
|
|
4123
4205
|
} else {
|
|
4124
4206
|
nextTools = explicitRequestedTools!;
|
|
@@ -4149,41 +4231,88 @@ export async function updateSessionToolPolicy(
|
|
|
4149
4231
|
requestedMode === "workspace_default"
|
|
4150
4232
|
? workspaceDefaultFirstPartyTools
|
|
4151
4233
|
: explicitRequestedFirstPartyTools!;
|
|
4152
|
-
nextPolicy = {
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4234
|
+
nextPolicy = {
|
|
4235
|
+
mode: requestedMode,
|
|
4236
|
+
inheritedFromSessionId: null,
|
|
4237
|
+
...(requestedMode === "workspace_default"
|
|
4238
|
+
? defaultPolicyExclusions(requestedExclusions)
|
|
4239
|
+
: {}),
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
if (connectorOnlyEdit) {
|
|
4243
|
+
if (session.toolPolicy.mode !== "workspace_default") {
|
|
4244
|
+
throw new HTTPException(409, {
|
|
4245
|
+
message: "adopt workspace defaults before editing connector exclusions",
|
|
4246
|
+
});
|
|
4247
|
+
}
|
|
4248
|
+
// A connector switch never rewrites built-in tool choices or selected refs.
|
|
4249
|
+
nextTools = session.tools;
|
|
4250
|
+
nextFirstPartyMcpTools = [
|
|
4251
|
+
...(session.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default),
|
|
4252
|
+
];
|
|
4253
|
+
}
|
|
4254
|
+
if (!connectorOnlyEdit) {
|
|
4255
|
+
nextTools = withoutExcludedMcpServers(nextTools, nextPolicy.excludedMcpServerIds);
|
|
4256
|
+
}
|
|
4257
|
+
if (agentAttemptCaller && !session.parentSessionId) {
|
|
4258
|
+
// A human or API key may widen a top-level session; a live agent
|
|
4259
|
+
// attempt may only narrow relative to the session's CURRENT
|
|
4260
|
+
// effective policy, in either mode. Adopting workspace defaults is a
|
|
4261
|
+
// widen whenever it adds a server or tool the session does not hold.
|
|
4262
|
+
const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
|
|
4263
|
+
const currentEffectiveTools = withFirstPartyTools(
|
|
4264
|
+
sessionTracksWorkspaceDefaults
|
|
4265
|
+
? withWorkspaceDefaultMcpTools(
|
|
4266
|
+
availableToolRefs(session.tools, runtimeSettings),
|
|
4267
|
+
deps.settings,
|
|
4268
|
+
runtimeSettings,
|
|
4269
|
+
workspaceSessionToolDefaults,
|
|
4270
|
+
)
|
|
4271
|
+
: session.tools,
|
|
4272
|
+
runtimeSettings,
|
|
4273
|
+
);
|
|
4274
|
+
const currentAllowedTools = withoutExcludedMcpServers(
|
|
4275
|
+
currentEffectiveTools,
|
|
4276
|
+
session.toolPolicy.excludedMcpServerIds,
|
|
4277
|
+
);
|
|
4278
|
+
const nextEffectiveTools =
|
|
4279
|
+
nextPolicy.mode === "workspace_default"
|
|
4280
|
+
? withFirstPartyTools(
|
|
4281
|
+
withWorkspaceDefaultMcpTools(
|
|
4282
|
+
availableToolRefs(nextTools, runtimeSettings),
|
|
4163
4283
|
deps.settings,
|
|
4164
4284
|
runtimeSettings,
|
|
4165
4285
|
workspaceSessionToolDefaults,
|
|
4166
|
-
)
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
)
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
);
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
)
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4286
|
+
),
|
|
4287
|
+
runtimeSettings,
|
|
4288
|
+
)
|
|
4289
|
+
: nextTools;
|
|
4290
|
+
if (
|
|
4291
|
+
nextPolicy.mode === "workspace_default" &&
|
|
4292
|
+
(session.toolPolicy.excludedMcpServerIds ?? []).some(
|
|
4293
|
+
(id) => !nextPolicy.excludedMcpServerIds?.includes(id),
|
|
4294
|
+
)
|
|
4295
|
+
) {
|
|
4296
|
+
throw new HTTPException(403, {
|
|
4297
|
+
message: "an agent may not remove session connector exclusions",
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
assertToolRefsSubset(
|
|
4301
|
+
withoutExcludedMcpServers(nextEffectiveTools, nextPolicy.excludedMcpServerIds),
|
|
4302
|
+
currentAllowedTools,
|
|
4303
|
+
"an agent may only narrow its session tool policy",
|
|
4304
|
+
);
|
|
4305
|
+
const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
|
|
4306
|
+
session.firstPartyMcpTools,
|
|
4307
|
+
deploymentFirstPartyMcpToolPolicy,
|
|
4308
|
+
);
|
|
4309
|
+
const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
|
|
4310
|
+
(tool) => !currentFirstPartyCeiling.has(tool),
|
|
4311
|
+
);
|
|
4312
|
+
if (widenedFirstPartyTool) {
|
|
4313
|
+
throw new HTTPException(403, {
|
|
4314
|
+
message: `an agent may only narrow its session OpenGeni tools: ${widenedFirstPartyTool}`,
|
|
4315
|
+
});
|
|
4187
4316
|
}
|
|
4188
4317
|
}
|
|
4189
4318
|
|