@opengeni/core 0.2.2 → 0.3.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/index.d.ts +16 -4
- package/dist/index.js +166 -10
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/domain/sessions.ts +206 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
39
|
"@opengeni/codex": "^0.2.1",
|
|
40
|
-
"@opengeni/config": "^0.2.
|
|
41
|
-
"@opengeni/contracts": "^0.
|
|
42
|
-
"@opengeni/db": "^0.
|
|
43
|
-
"@opengeni/documents": "^0.2.
|
|
44
|
-
"@opengeni/events": "^0.2.
|
|
40
|
+
"@opengeni/config": "^0.2.3",
|
|
41
|
+
"@opengeni/contracts": "^0.5.0",
|
|
42
|
+
"@opengeni/db": "^0.3.0",
|
|
43
|
+
"@opengeni/documents": "^0.2.3",
|
|
44
|
+
"@opengeni/events": "^0.2.3",
|
|
45
45
|
"@opengeni/observability": "^0.2.1",
|
|
46
|
-
"@opengeni/runtime": "^0.2.
|
|
47
|
-
"@opengeni/storage": "^0.2.
|
|
46
|
+
"@opengeni/runtime": "^0.2.3",
|
|
47
|
+
"@opengeni/storage": "^0.2.3",
|
|
48
48
|
"hono": "^4.12.18"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
package/src/domain/sessions.ts
CHANGED
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
type ResourceRef,
|
|
11
11
|
type Session,
|
|
12
12
|
type SessionEvent,
|
|
13
|
+
type SessionMcpCredentialUpdateInput,
|
|
14
|
+
type SessionMcpServerInput,
|
|
15
|
+
type SessionMcpServerMetadata,
|
|
13
16
|
type SessionTurn,
|
|
14
17
|
type ToolRef,
|
|
15
18
|
} from "@opengeni/contracts";
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
createSessionGoal,
|
|
20
23
|
createSessionWithIdempotencyKey,
|
|
21
24
|
enqueueSessionTurn,
|
|
25
|
+
encryptEnvironmentValue,
|
|
22
26
|
getAnySessionInGroup,
|
|
23
27
|
getEnrollment,
|
|
24
28
|
listDistinctEnvironmentIdsInGroup,
|
|
@@ -29,16 +33,18 @@ import {
|
|
|
29
33
|
requireSession,
|
|
30
34
|
setTemporalWorkflowId,
|
|
31
35
|
updateSessionTitle as updateSessionTitleRow,
|
|
36
|
+
type CreateSessionMcpServerInput,
|
|
32
37
|
type Database,
|
|
38
|
+
type UpdateSessionMcpServerCredentialsInput,
|
|
33
39
|
} from "@opengeni/db";
|
|
34
40
|
import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
|
|
35
41
|
import { HTTPException } from "hono/http-exception";
|
|
36
|
-
import { hasPermission } from "../access";
|
|
42
|
+
import { hasPermission, requirePermission } from "../access";
|
|
37
43
|
import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
|
|
38
44
|
import type { ApiRouteDeps, SessionWorkflowClient } from "../dependencies";
|
|
39
45
|
import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
40
46
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
41
|
-
import { validateEnvironmentAttachment } from "./environments";
|
|
47
|
+
import { requireEnvironmentEncryption, validateEnvironmentAttachment } from "./environments";
|
|
42
48
|
import {
|
|
43
49
|
mergeResourceRefs,
|
|
44
50
|
mergeToolRefs,
|
|
@@ -49,6 +55,166 @@ import {
|
|
|
49
55
|
withDefaultEnabledCapabilityMcpTools,
|
|
50
56
|
} from "./resources";
|
|
51
57
|
|
|
58
|
+
const reservedSessionMcpServerIds = new Set(["opengeni", "files", "docs", "codex_apps"]);
|
|
59
|
+
const maxSessionMcpCredentialHeaders = 16;
|
|
60
|
+
const maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
61
|
+
// RFC 9110 field-name token characters.
|
|
62
|
+
const sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
63
|
+
|
|
64
|
+
type ValidatedSessionMcpServers = {
|
|
65
|
+
runtimeServers: Settings["mcpServers"];
|
|
66
|
+
dbServers: CreateSessionMcpServerInput[];
|
|
67
|
+
metadata: SessionMcpServerMetadata[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function normalizedSessionMcpCredentialHeaders(headers: Record<string, string> | undefined): Record<string, string> {
|
|
71
|
+
if (!headers) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value] as const).filter(([name]) => name.length > 0);
|
|
75
|
+
if (entries.length > maxSessionMcpCredentialHeaders) {
|
|
76
|
+
throw new HTTPException(422, { message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers` });
|
|
77
|
+
}
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
for (const [name, value] of entries) {
|
|
80
|
+
if (!sessionMcpCredentialHeaderName.test(name)) {
|
|
81
|
+
throw new HTTPException(422, { message: `invalid credential header name: ${name}` });
|
|
82
|
+
}
|
|
83
|
+
const lower = name.toLowerCase();
|
|
84
|
+
if (seen.has(lower)) {
|
|
85
|
+
throw new HTTPException(422, { message: `duplicate credential header name: ${name}` });
|
|
86
|
+
}
|
|
87
|
+
seen.add(lower);
|
|
88
|
+
if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
|
|
89
|
+
throw new HTTPException(422, { message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters` });
|
|
90
|
+
}
|
|
91
|
+
// RFC 9110 §5.5: field values are HTAB / printable characters.
|
|
92
|
+
// eslint-disable-next-line no-control-regex
|
|
93
|
+
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
94
|
+
throw new HTTPException(422, { message: `credential header ${name} contains forbidden control characters` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return Object.fromEntries(entries);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mcpServerConfigFromInput(server: SessionMcpServerInput): Settings["mcpServers"][number] {
|
|
101
|
+
return {
|
|
102
|
+
id: server.id,
|
|
103
|
+
...(server.name ? { name: server.name } : {}),
|
|
104
|
+
url: server.url,
|
|
105
|
+
...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
|
|
106
|
+
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
107
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mcpServerConfigFromMetadata(server: SessionMcpServerMetadata): Settings["mcpServers"][number] {
|
|
112
|
+
return {
|
|
113
|
+
id: server.id,
|
|
114
|
+
...(server.name ? { name: server.name } : {}),
|
|
115
|
+
url: server.url,
|
|
116
|
+
cacheToolsList: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function settingsWithSessionMcpServerConfigs(settings: Settings, servers: Settings["mcpServers"]): Settings {
|
|
121
|
+
if (servers.length === 0) {
|
|
122
|
+
return settings;
|
|
123
|
+
}
|
|
124
|
+
const sessionIds = new Set(servers.map((server) => server.id));
|
|
125
|
+
return {
|
|
126
|
+
...settings,
|
|
127
|
+
mcpServers: [
|
|
128
|
+
...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
|
|
129
|
+
...servers,
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function settingsWithSessionMcpServerMetadata(settings: Settings, servers: SessionMcpServerMetadata[]): Settings {
|
|
135
|
+
return settingsWithSessionMcpServerConfigs(settings, servers.map(mcpServerConfigFromMetadata));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateSessionMcpServersForCreate(
|
|
139
|
+
settings: Settings,
|
|
140
|
+
grant: AccessGrant,
|
|
141
|
+
servers: SessionMcpServerInput[],
|
|
142
|
+
): ValidatedSessionMcpServers {
|
|
143
|
+
if (servers.length === 0) {
|
|
144
|
+
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
145
|
+
}
|
|
146
|
+
requirePermission(grant, "mcp_servers:attach");
|
|
147
|
+
const encryptionKey = requireEnvironmentEncryption(settings);
|
|
148
|
+
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
149
|
+
const seenIds = new Set<string>();
|
|
150
|
+
const runtimeServers: Settings["mcpServers"] = [];
|
|
151
|
+
const dbServers: CreateSessionMcpServerInput[] = [];
|
|
152
|
+
const metadata: SessionMcpServerMetadata[] = [];
|
|
153
|
+
for (const server of servers) {
|
|
154
|
+
if (seenIds.has(server.id)) {
|
|
155
|
+
throw new HTTPException(422, { message: `duplicate session MCP server id: ${server.id}` });
|
|
156
|
+
}
|
|
157
|
+
seenIds.add(server.id);
|
|
158
|
+
if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
|
|
159
|
+
throw new HTTPException(422, { message: `MCP server id already exists: ${server.id}` });
|
|
160
|
+
}
|
|
161
|
+
const headers = normalizedSessionMcpCredentialHeaders(server.headers);
|
|
162
|
+
const headersEncrypted = Object.fromEntries(
|
|
163
|
+
Object.entries(headers).map(([name, value]) => [name, encryptEnvironmentValue(encryptionKey, value)]),
|
|
164
|
+
);
|
|
165
|
+
runtimeServers.push(mcpServerConfigFromInput(server));
|
|
166
|
+
dbServers.push({
|
|
167
|
+
id: server.id,
|
|
168
|
+
name: server.name ?? null,
|
|
169
|
+
url: server.url,
|
|
170
|
+
allowedTools: server.allowedTools ?? null,
|
|
171
|
+
timeoutMs: server.timeoutMs ?? null,
|
|
172
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
173
|
+
headersEncrypted,
|
|
174
|
+
});
|
|
175
|
+
metadata.push({
|
|
176
|
+
id: server.id,
|
|
177
|
+
name: server.name ?? null,
|
|
178
|
+
url: server.url,
|
|
179
|
+
headerNames: Object.keys(headersEncrypted).sort(),
|
|
180
|
+
credentialVersion: 1,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return { runtimeServers, dbServers, metadata };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateSessionMcpCredentialUpdates(input: {
|
|
187
|
+
settings: Settings;
|
|
188
|
+
grant: AccessGrant;
|
|
189
|
+
session: Session;
|
|
190
|
+
updates: SessionMcpCredentialUpdateInput[];
|
|
191
|
+
}): UpdateSessionMcpServerCredentialsInput[] {
|
|
192
|
+
if (input.updates.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
requirePermission(input.grant, "mcp_servers:attach");
|
|
196
|
+
const encryptionKey = requireEnvironmentEncryption(input.settings);
|
|
197
|
+
const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
|
|
198
|
+
const seenIds = new Set<string>();
|
|
199
|
+
const encryptedUpdates = input.updates.map((update) => {
|
|
200
|
+
if (seenIds.has(update.id)) {
|
|
201
|
+
throw new HTTPException(422, { message: `duplicate session MCP credential update id: ${update.id}` });
|
|
202
|
+
}
|
|
203
|
+
seenIds.add(update.id);
|
|
204
|
+
if (!knownIds.has(update.id)) {
|
|
205
|
+
throw new HTTPException(422, { message: `unknown session MCP server id: ${update.id}` });
|
|
206
|
+
}
|
|
207
|
+
const headers = normalizedSessionMcpCredentialHeaders(update.headers);
|
|
208
|
+
return {
|
|
209
|
+
id: update.id,
|
|
210
|
+
headersEncrypted: Object.fromEntries(
|
|
211
|
+
Object.entries(headers).map(([name, value]) => [name, encryptEnvironmentValue(encryptionKey, value)]),
|
|
212
|
+
),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
return encryptedUpdates;
|
|
216
|
+
}
|
|
217
|
+
|
|
52
218
|
export async function createAndStartSession(input: {
|
|
53
219
|
db: Database;
|
|
54
220
|
bus: EventBus;
|
|
@@ -68,6 +234,10 @@ export async function createAndStartSession(input: {
|
|
|
68
234
|
goal?: GoalSpec | null;
|
|
69
235
|
// Validated against the creating grant before this is called.
|
|
70
236
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
237
|
+
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
238
|
+
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
239
|
+
mcpServers?: CreateSessionMcpServerInput[];
|
|
240
|
+
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
71
241
|
// The manager session spawning this worker (a worker-signed sessionId claim
|
|
72
242
|
// on the creating grant); null for direct API creates and scheduled runs.
|
|
73
243
|
// When set, the worker's terminal-for-now transitions wake this parent.
|
|
@@ -127,6 +297,7 @@ export async function createAndStartSession(input: {
|
|
|
127
297
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
128
298
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
129
299
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
300
|
+
mcpServers: input.mcpServers ?? [],
|
|
130
301
|
});
|
|
131
302
|
if (!created) {
|
|
132
303
|
return keyed;
|
|
@@ -147,6 +318,7 @@ export async function createAndStartSession(input: {
|
|
|
147
318
|
parentSessionId: input.parentSessionId ?? null,
|
|
148
319
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
149
320
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
321
|
+
mcpServers: input.mcpServers ?? [],
|
|
150
322
|
});
|
|
151
323
|
return await finishStartSession(input, session);
|
|
152
324
|
}
|
|
@@ -171,6 +343,7 @@ async function finishStartSession(input: {
|
|
|
171
343
|
sandboxBackend: Settings["sandboxBackend"];
|
|
172
344
|
environment?: { id: string; name: string } | null;
|
|
173
345
|
goal?: GoalSpec | null;
|
|
346
|
+
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
174
347
|
seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
|
|
175
348
|
}, session: Session): Promise<Session> {
|
|
176
349
|
// The goal row is durable session state; the workflow picks it up from the
|
|
@@ -197,6 +370,7 @@ async function finishStartSession(input: {
|
|
|
197
370
|
payload: {
|
|
198
371
|
status: "queued",
|
|
199
372
|
...(input.environment ? { environmentId: input.environment.id, environmentName: input.environment.name } : {}),
|
|
373
|
+
...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
|
|
200
374
|
},
|
|
201
375
|
},
|
|
202
376
|
...(goal ? [{
|
|
@@ -355,6 +529,7 @@ export async function postUserMessageTurn(input: {
|
|
|
355
529
|
model?: string | null;
|
|
356
530
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
357
531
|
clientEventId?: string;
|
|
532
|
+
mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
|
|
358
533
|
}): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
359
534
|
const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
|
|
360
535
|
const requestedModel = input.model ?? null;
|
|
@@ -362,7 +537,7 @@ export async function postUserMessageTurn(input: {
|
|
|
362
537
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
363
538
|
// model inherits the session's model downstream (always a configured id).
|
|
364
539
|
assertConfiguredModel(settings, requestedModel);
|
|
365
|
-
const appended = await appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, (lockedSession) => {
|
|
540
|
+
const appended = await appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, async (lockedSession, lockedUpdate) => {
|
|
366
541
|
// Cancelled is the one terminal state: an explicit user act. A FAILED
|
|
367
542
|
// session stays revivable by talking to it — conversation truth lives in
|
|
368
543
|
// session_history_items, so a failed turn does not invalidate history,
|
|
@@ -373,6 +548,12 @@ export async function postUserMessageTurn(input: {
|
|
|
373
548
|
if (lockedSession.status === "cancelled") {
|
|
374
549
|
throw new HTTPException(409, { message: `session is ${lockedSession.status}; cannot accept a new user message` });
|
|
375
550
|
}
|
|
551
|
+
const mcpCredentialUpdates = input.mcpCredentialUpdates?.length
|
|
552
|
+
? await lockedUpdate.updateSessionMcpServerCredentials(input.mcpCredentialUpdates)
|
|
553
|
+
: { servers: [], missingIds: [] };
|
|
554
|
+
if (mcpCredentialUpdates.missingIds.length > 0) {
|
|
555
|
+
throw new HTTPException(422, { message: `unknown session MCP server id: ${mcpCredentialUpdates.missingIds[0]}` });
|
|
556
|
+
}
|
|
376
557
|
const nextResources = mergeResourceRefs(lockedSession.resources, input.resources);
|
|
377
558
|
const nextTools = mergeToolRefs(lockedSession.tools, input.tools);
|
|
378
559
|
const shouldQueueSession = lockedSession.status === "idle" || lockedSession.status === "failed";
|
|
@@ -386,6 +567,7 @@ export async function postUserMessageTurn(input: {
|
|
|
386
567
|
...(input.tools.length ? { tools: input.tools } : {}),
|
|
387
568
|
...(requestedModel ? { model: requestedModel } : {}),
|
|
388
569
|
...(requestedReasoningEffort ? { reasoningEffort: requestedReasoningEffort } : {}),
|
|
570
|
+
...(mcpCredentialUpdates.servers.length ? { mcpCredentialUpdates: mcpCredentialUpdates.servers } : {}),
|
|
389
571
|
},
|
|
390
572
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
391
573
|
},
|
|
@@ -446,12 +628,14 @@ export async function createSessionForRequest(
|
|
|
446
628
|
): Promise<Session> {
|
|
447
629
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
448
630
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
449
|
-
const
|
|
631
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
632
|
+
const sessionMcpServers = validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
|
|
633
|
+
const runtimeSettings = settingsWithSessionMcpServerConfigs(capabilityRuntimeSettings, sessionMcpServers.runtimeServers);
|
|
450
634
|
const resources = normalizeResources(payload.resources);
|
|
451
635
|
const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
|
|
452
636
|
const defaultedTools = hasOwnProperty(rawPayload, "tools")
|
|
453
637
|
? requestedTools
|
|
454
|
-
: withDefaultEnabledCapabilityMcpTools(requestedTools, settings,
|
|
638
|
+
: withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
|
|
455
639
|
// The first-party MCP server is attached to EVERY session. It hosts the
|
|
456
640
|
// session's own metadata tool (set_session_title) + goal tools, and — only
|
|
457
641
|
// when the grant carries the permission — the orchestration/environment/
|
|
@@ -672,6 +856,8 @@ export async function createSessionForRequest(
|
|
|
672
856
|
environment: environment ? { id: environment.id, name: environment.name } : null,
|
|
673
857
|
goal: payload.goal ?? null,
|
|
674
858
|
firstPartyMcpPermissions,
|
|
859
|
+
mcpServers: sessionMcpServers.dbServers,
|
|
860
|
+
sessionMcpServers: sessionMcpServers.metadata,
|
|
675
861
|
parentSessionId,
|
|
676
862
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
677
863
|
// Create-time machine targeting (A-2a): when a target sandbox is named, the
|
|
@@ -716,19 +902,21 @@ export async function acceptSessionUserMessage(
|
|
|
716
902
|
model?: string | null;
|
|
717
903
|
reasoningEffort?: ReasoningEffort | null;
|
|
718
904
|
clientEventId?: string;
|
|
905
|
+
mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
|
|
719
906
|
},
|
|
720
907
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
721
908
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
722
|
-
const
|
|
723
|
-
const requestedResources = normalizeResources(input.resources ?? []);
|
|
724
|
-
const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
|
|
725
|
-
const requestedTools = input.toolsProvided
|
|
726
|
-
? validatedTools
|
|
727
|
-
: withDefaultEnabledCapabilityMcpTools(validatedTools, settings, runtimeSettings);
|
|
909
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
728
910
|
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
729
911
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
730
912
|
// pure read with no side effects.
|
|
731
913
|
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
914
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(capabilityRuntimeSettings, existingSession.mcpServers);
|
|
915
|
+
const requestedResources = normalizeResources(input.resources ?? []);
|
|
916
|
+
const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
|
|
917
|
+
const requestedTools = input.toolsProvided
|
|
918
|
+
? validatedTools
|
|
919
|
+
: withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
|
|
732
920
|
await requireLimit(deps, {
|
|
733
921
|
accountId: grant.accountId,
|
|
734
922
|
workspaceId,
|
|
@@ -741,6 +929,12 @@ export async function acceptSessionUserMessage(
|
|
|
741
929
|
}
|
|
742
930
|
await validateFileResources(db, workspaceId, requestedResources);
|
|
743
931
|
await validateGitHubRepositorySelection(db, workspaceId, [...existingSession.resources, ...requestedResources]);
|
|
932
|
+
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
933
|
+
settings,
|
|
934
|
+
grant,
|
|
935
|
+
session: existingSession,
|
|
936
|
+
updates: input.mcpCredentialUpdates ?? [],
|
|
937
|
+
});
|
|
744
938
|
const { accepted, turn } = await postUserMessageTurn({
|
|
745
939
|
db,
|
|
746
940
|
bus,
|
|
@@ -754,6 +948,7 @@ export async function acceptSessionUserMessage(
|
|
|
754
948
|
tools: requestedTools,
|
|
755
949
|
model: input.model ?? null,
|
|
756
950
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
951
|
+
mcpCredentialUpdates,
|
|
757
952
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
758
953
|
});
|
|
759
954
|
await recordWorkspaceUsage(deps, {
|