@opengeni/core 0.2.2 → 0.4.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 +18 -4
- package/dist/index.js +172 -10
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/domain/sessions.ts +217 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4",
|
|
41
|
+
"@opengeni/contracts": "^0.6.0",
|
|
42
|
+
"@opengeni/db": "^0.4.0",
|
|
43
|
+
"@opengeni/documents": "^0.2.4",
|
|
44
|
+
"@opengeni/events": "^0.2.4",
|
|
45
45
|
"@opengeni/observability": "^0.2.1",
|
|
46
|
-
"@opengeni/runtime": "^0.
|
|
47
|
-
"@opengeni/storage": "^0.2.
|
|
46
|
+
"@opengeni/runtime": "^0.3.0",
|
|
47
|
+
"@opengeni/storage": "^0.2.4",
|
|
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;
|
|
@@ -66,8 +232,17 @@ export async function createAndStartSession(input: {
|
|
|
66
232
|
// Names/ids only; the session.created payload never carries variable values.
|
|
67
233
|
environment?: { id: string; name: string } | null;
|
|
68
234
|
goal?: GoalSpec | null;
|
|
235
|
+
// Per-session agent persona/system instructions (org-visible metadata, not a
|
|
236
|
+
// secret). Persisted on the session row and composed system-level AFTER the
|
|
237
|
+
// workspace agentInstructions at turn time; never emitted as a timeline event.
|
|
238
|
+
// Null/omitted ⇒ the session carries none.
|
|
239
|
+
instructions?: string | null;
|
|
69
240
|
// Validated against the creating grant before this is called.
|
|
70
241
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
242
|
+
// Encrypted DB rows plus matching safe metadata for create-time per-session
|
|
243
|
+
// MCP servers. Metadata is the only shape emitted in events/responses.
|
|
244
|
+
mcpServers?: CreateSessionMcpServerInput[];
|
|
245
|
+
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
71
246
|
// The manager session spawning this worker (a worker-signed sessionId claim
|
|
72
247
|
// on the creating grant); null for direct API creates and scheduled runs.
|
|
73
248
|
// When set, the worker's terminal-for-now transitions wake this parent.
|
|
@@ -123,10 +298,12 @@ export async function createAndStartSession(input: {
|
|
|
123
298
|
sandboxBackend: input.sandboxBackend,
|
|
124
299
|
environmentId: input.environment?.id ?? null,
|
|
125
300
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
301
|
+
instructions: input.instructions ?? null,
|
|
126
302
|
parentSessionId: input.parentSessionId ?? null,
|
|
127
303
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
128
304
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
129
305
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
306
|
+
mcpServers: input.mcpServers ?? [],
|
|
130
307
|
});
|
|
131
308
|
if (!created) {
|
|
132
309
|
return keyed;
|
|
@@ -144,9 +321,11 @@ export async function createAndStartSession(input: {
|
|
|
144
321
|
sandboxBackend: input.sandboxBackend,
|
|
145
322
|
environmentId: input.environment?.id ?? null,
|
|
146
323
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
324
|
+
instructions: input.instructions ?? null,
|
|
147
325
|
parentSessionId: input.parentSessionId ?? null,
|
|
148
326
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
149
327
|
...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
|
|
328
|
+
mcpServers: input.mcpServers ?? [],
|
|
150
329
|
});
|
|
151
330
|
return await finishStartSession(input, session);
|
|
152
331
|
}
|
|
@@ -171,6 +350,7 @@ async function finishStartSession(input: {
|
|
|
171
350
|
sandboxBackend: Settings["sandboxBackend"];
|
|
172
351
|
environment?: { id: string; name: string } | null;
|
|
173
352
|
goal?: GoalSpec | null;
|
|
353
|
+
sessionMcpServers?: SessionMcpServerMetadata[];
|
|
174
354
|
seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
|
|
175
355
|
}, session: Session): Promise<Session> {
|
|
176
356
|
// The goal row is durable session state; the workflow picks it up from the
|
|
@@ -197,6 +377,7 @@ async function finishStartSession(input: {
|
|
|
197
377
|
payload: {
|
|
198
378
|
status: "queued",
|
|
199
379
|
...(input.environment ? { environmentId: input.environment.id, environmentName: input.environment.name } : {}),
|
|
380
|
+
...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
|
|
200
381
|
},
|
|
201
382
|
},
|
|
202
383
|
...(goal ? [{
|
|
@@ -355,6 +536,7 @@ export async function postUserMessageTurn(input: {
|
|
|
355
536
|
model?: string | null;
|
|
356
537
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
357
538
|
clientEventId?: string;
|
|
539
|
+
mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
|
|
358
540
|
}): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
359
541
|
const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
|
|
360
542
|
const requestedModel = input.model ?? null;
|
|
@@ -362,7 +544,7 @@ export async function postUserMessageTurn(input: {
|
|
|
362
544
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
363
545
|
// model inherits the session's model downstream (always a configured id).
|
|
364
546
|
assertConfiguredModel(settings, requestedModel);
|
|
365
|
-
const appended = await appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, (lockedSession) => {
|
|
547
|
+
const appended = await appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, async (lockedSession, lockedUpdate) => {
|
|
366
548
|
// Cancelled is the one terminal state: an explicit user act. A FAILED
|
|
367
549
|
// session stays revivable by talking to it — conversation truth lives in
|
|
368
550
|
// session_history_items, so a failed turn does not invalidate history,
|
|
@@ -373,6 +555,12 @@ export async function postUserMessageTurn(input: {
|
|
|
373
555
|
if (lockedSession.status === "cancelled") {
|
|
374
556
|
throw new HTTPException(409, { message: `session is ${lockedSession.status}; cannot accept a new user message` });
|
|
375
557
|
}
|
|
558
|
+
const mcpCredentialUpdates = input.mcpCredentialUpdates?.length
|
|
559
|
+
? await lockedUpdate.updateSessionMcpServerCredentials(input.mcpCredentialUpdates)
|
|
560
|
+
: { servers: [], missingIds: [] };
|
|
561
|
+
if (mcpCredentialUpdates.missingIds.length > 0) {
|
|
562
|
+
throw new HTTPException(422, { message: `unknown session MCP server id: ${mcpCredentialUpdates.missingIds[0]}` });
|
|
563
|
+
}
|
|
376
564
|
const nextResources = mergeResourceRefs(lockedSession.resources, input.resources);
|
|
377
565
|
const nextTools = mergeToolRefs(lockedSession.tools, input.tools);
|
|
378
566
|
const shouldQueueSession = lockedSession.status === "idle" || lockedSession.status === "failed";
|
|
@@ -386,6 +574,7 @@ export async function postUserMessageTurn(input: {
|
|
|
386
574
|
...(input.tools.length ? { tools: input.tools } : {}),
|
|
387
575
|
...(requestedModel ? { model: requestedModel } : {}),
|
|
388
576
|
...(requestedReasoningEffort ? { reasoningEffort: requestedReasoningEffort } : {}),
|
|
577
|
+
...(mcpCredentialUpdates.servers.length ? { mcpCredentialUpdates: mcpCredentialUpdates.servers } : {}),
|
|
389
578
|
},
|
|
390
579
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
391
580
|
},
|
|
@@ -446,12 +635,14 @@ export async function createSessionForRequest(
|
|
|
446
635
|
): Promise<Session> {
|
|
447
636
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
448
637
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
449
|
-
const
|
|
638
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
639
|
+
const sessionMcpServers = validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
|
|
640
|
+
const runtimeSettings = settingsWithSessionMcpServerConfigs(capabilityRuntimeSettings, sessionMcpServers.runtimeServers);
|
|
450
641
|
const resources = normalizeResources(payload.resources);
|
|
451
642
|
const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
|
|
452
643
|
const defaultedTools = hasOwnProperty(rawPayload, "tools")
|
|
453
644
|
? requestedTools
|
|
454
|
-
: withDefaultEnabledCapabilityMcpTools(requestedTools, settings,
|
|
645
|
+
: withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
|
|
455
646
|
// The first-party MCP server is attached to EVERY session. It hosts the
|
|
456
647
|
// session's own metadata tool (set_session_title) + goal tools, and — only
|
|
457
648
|
// when the grant carries the permission — the orchestration/environment/
|
|
@@ -671,7 +862,13 @@ export async function createSessionForRequest(
|
|
|
671
862
|
metadata: payload.metadata,
|
|
672
863
|
environment: environment ? { id: environment.id, name: environment.name } : null,
|
|
673
864
|
goal: payload.goal ?? null,
|
|
865
|
+
// Per-session persona instructions (already trimmed/validated by the
|
|
866
|
+
// contracts schema). Persisted on the row; composed system-level at turn
|
|
867
|
+
// time. Not surfaced as an event.
|
|
868
|
+
instructions: payload.instructions ?? null,
|
|
674
869
|
firstPartyMcpPermissions,
|
|
870
|
+
mcpServers: sessionMcpServers.dbServers,
|
|
871
|
+
sessionMcpServers: sessionMcpServers.metadata,
|
|
675
872
|
parentSessionId,
|
|
676
873
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
677
874
|
// Create-time machine targeting (A-2a): when a target sandbox is named, the
|
|
@@ -716,19 +913,21 @@ export async function acceptSessionUserMessage(
|
|
|
716
913
|
model?: string | null;
|
|
717
914
|
reasoningEffort?: ReasoningEffort | null;
|
|
718
915
|
clientEventId?: string;
|
|
916
|
+
mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
|
|
719
917
|
},
|
|
720
918
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
721
919
|
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);
|
|
920
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
728
921
|
// Hoisted above requireLimit so the codex-billed predicate can resolve the
|
|
729
922
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
730
923
|
// pure read with no side effects.
|
|
731
924
|
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
925
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(capabilityRuntimeSettings, existingSession.mcpServers);
|
|
926
|
+
const requestedResources = normalizeResources(input.resources ?? []);
|
|
927
|
+
const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
|
|
928
|
+
const requestedTools = input.toolsProvided
|
|
929
|
+
? validatedTools
|
|
930
|
+
: withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
|
|
732
931
|
await requireLimit(deps, {
|
|
733
932
|
accountId: grant.accountId,
|
|
734
933
|
workspaceId,
|
|
@@ -741,6 +940,12 @@ export async function acceptSessionUserMessage(
|
|
|
741
940
|
}
|
|
742
941
|
await validateFileResources(db, workspaceId, requestedResources);
|
|
743
942
|
await validateGitHubRepositorySelection(db, workspaceId, [...existingSession.resources, ...requestedResources]);
|
|
943
|
+
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
944
|
+
settings,
|
|
945
|
+
grant,
|
|
946
|
+
session: existingSession,
|
|
947
|
+
updates: input.mcpCredentialUpdates ?? [],
|
|
948
|
+
});
|
|
744
949
|
const { accepted, turn } = await postUserMessageTurn({
|
|
745
950
|
db,
|
|
746
951
|
bus,
|
|
@@ -754,6 +959,7 @@ export async function acceptSessionUserMessage(
|
|
|
754
959
|
tools: requestedTools,
|
|
755
960
|
model: input.model ?? null,
|
|
756
961
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
962
|
+
mcpCredentialUpdates,
|
|
757
963
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
758
964
|
});
|
|
759
965
|
await recordWorkspaceUsage(deps, {
|