@opengeni/core 0.4.10 → 0.10.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 +165 -141
- package/dist/index.js +1132 -186
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/access/index.ts +4 -0
- package/src/application/session-commands.ts +192 -38
- package/src/billing/limits.ts +20 -1
- package/src/dependencies.ts +28 -2
- package/src/domain/capabilities.ts +171 -3
- package/src/domain/resources.ts +121 -31
- package/src/domain/scheduled-tasks.ts +4 -8
- package/src/domain/session-tool-policy.ts +211 -0
- package/src/domain/sessions.ts +658 -122
- package/src/index.ts +2 -0
- package/src/sandbox/fleet.ts +2 -2
- package/src/sandbox/routing.ts +1 -1
- package/src/session-authorization.ts +208 -0
package/src/domain/sessions.ts
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
canonicalizeConfiguredModelId,
|
|
4
|
+
configuredAllowedModels,
|
|
5
|
+
policyProviderIdForModel,
|
|
6
|
+
resolveTurnExecutionPolicyV1,
|
|
7
|
+
type Settings,
|
|
8
|
+
} from "@opengeni/config";
|
|
3
9
|
import {
|
|
4
10
|
CreateSessionRequest,
|
|
11
|
+
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
12
|
+
ServiceTurnInitiator,
|
|
13
|
+
ServiceTurnInitiatorContext,
|
|
5
14
|
evaluateWorkspaceModelPolicy,
|
|
6
15
|
reasoningEffortForMetadata,
|
|
7
16
|
type AccessGrant,
|
|
17
|
+
type CreateSessionResponse,
|
|
8
18
|
type GoalSpec,
|
|
9
19
|
type Permission,
|
|
10
20
|
type ReasoningEffort,
|
|
11
21
|
type ResourceRef,
|
|
12
22
|
type Session,
|
|
13
23
|
type SessionEvent,
|
|
24
|
+
SessionMcpApprovalPolicy,
|
|
14
25
|
type SessionMcpCredentialUpdateInput,
|
|
15
26
|
type SessionMcpServerInput,
|
|
16
27
|
type SessionMcpServerMetadata,
|
|
28
|
+
type UpdateSessionMcpApprovalPolicyResponse,
|
|
29
|
+
type SessionAuthorizationPort,
|
|
30
|
+
type SessionToolPolicy,
|
|
17
31
|
type SessionTurn,
|
|
18
32
|
type ToolRef,
|
|
33
|
+
type TurnInitiator,
|
|
34
|
+
type TurnInitiatorContext,
|
|
35
|
+
type TurnExecutionPolicyV1,
|
|
19
36
|
} from "@opengeni/contracts";
|
|
20
37
|
import {
|
|
21
38
|
createSession,
|
|
@@ -29,6 +46,7 @@ import {
|
|
|
29
46
|
listDistinctRigVersionIdsInGroup,
|
|
30
47
|
getSandbox,
|
|
31
48
|
getSession,
|
|
49
|
+
SessionIdConflictError,
|
|
32
50
|
getSessionByCreateIdempotencyKey,
|
|
33
51
|
getSessionEvent,
|
|
34
52
|
getWorkspaceControlEvent,
|
|
@@ -36,15 +54,20 @@ import {
|
|
|
36
54
|
getSessionTurn,
|
|
37
55
|
getWorkspaceModelPolicy,
|
|
38
56
|
initializeSessionStartAtomically,
|
|
57
|
+
listSessionTurns,
|
|
58
|
+
listSessionMcpServersForChildInheritance,
|
|
39
59
|
requireSession,
|
|
40
60
|
submitHumanPromptInTransaction,
|
|
61
|
+
appendSessionEventsWithLockedSessionUpdate,
|
|
41
62
|
updateSessionTitle as updateSessionTitleRow,
|
|
42
63
|
withWorkspaceSubjectRls,
|
|
43
64
|
type CreateSessionMcpServerInput,
|
|
44
65
|
type Database,
|
|
45
66
|
type UpdateSessionMcpServerCredentialsInput,
|
|
46
67
|
QueueCommandConflictError,
|
|
68
|
+
AgentCommandAuthorityError,
|
|
47
69
|
SessionControlConflictError,
|
|
70
|
+
type SessionCommandActor,
|
|
48
71
|
} from "@opengeni/db";
|
|
49
72
|
import {
|
|
50
73
|
appendAndPublishEvents,
|
|
@@ -60,15 +83,19 @@ import type {
|
|
|
60
83
|
ApiRouteDeps,
|
|
61
84
|
SessionWorkflowClient,
|
|
62
85
|
} from "../dependencies";
|
|
86
|
+
import { requireSessionAuthorization } from "../session-authorization";
|
|
63
87
|
import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
64
88
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
65
89
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
66
90
|
import {
|
|
91
|
+
assertToolRefsSubset,
|
|
92
|
+
availableToolRefs,
|
|
67
93
|
mergeToolRefs,
|
|
68
94
|
normalizeResources,
|
|
69
95
|
validateFileResources,
|
|
70
96
|
validateGitHubRepositorySelection,
|
|
71
97
|
validateToolRefs,
|
|
98
|
+
validateToolRefsForSessionPolicy,
|
|
72
99
|
withDefaultEnabledCapabilityMcpTools,
|
|
73
100
|
} from "./resources";
|
|
74
101
|
|
|
@@ -84,6 +111,99 @@ type ValidatedSessionMcpServers = {
|
|
|
84
111
|
metadata: SessionMcpServerMetadata[];
|
|
85
112
|
};
|
|
86
113
|
|
|
114
|
+
type FrozenCreationInitiator = {
|
|
115
|
+
initiator?: TurnInitiator;
|
|
116
|
+
context?: TurnInitiatorContext;
|
|
117
|
+
actor?: Extract<SessionCommandActor, { type: "agent_attempt" }>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function serviceInitiatorForGrant(grant: AccessGrant): {
|
|
121
|
+
initiator: ServiceTurnInitiator;
|
|
122
|
+
context: ServiceTurnInitiatorContext;
|
|
123
|
+
} | null {
|
|
124
|
+
if (!grant.serviceInitiator) {
|
|
125
|
+
if (grant.serviceInitiatorContext) {
|
|
126
|
+
throw new HTTPException(403, {
|
|
127
|
+
message: "service initiator context requires a signed service initiator",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const initiator = ServiceTurnInitiator.safeParse(grant.serviceInitiator);
|
|
133
|
+
if (!initiator.success) {
|
|
134
|
+
throw new HTTPException(403, {
|
|
135
|
+
message: "a delegated command initiator must be a bounded service principal",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const context = ServiceTurnInitiatorContext.safeParse(grant.serviceInitiatorContext ?? {});
|
|
139
|
+
if (!context.success) {
|
|
140
|
+
throw new HTTPException(403, {
|
|
141
|
+
message: "delegated service initiator context is invalid or reserved",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const callerTurnId = grant.metadata?.["turnId"];
|
|
145
|
+
const callerAttemptId = grant.metadata?.["attemptId"];
|
|
146
|
+
const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
|
|
147
|
+
if (
|
|
148
|
+
callerTurnId !== undefined ||
|
|
149
|
+
callerAttemptId !== undefined ||
|
|
150
|
+
callerExecutionGeneration !== undefined
|
|
151
|
+
) {
|
|
152
|
+
throw new HTTPException(403, {
|
|
153
|
+
message: "a service initiator cannot replace an exact agent-attempt initiator",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
initiator: initiator.data,
|
|
158
|
+
context: context.data,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function creationInitiatorForGrant(grant: AccessGrant): FrozenCreationInitiator {
|
|
163
|
+
const serviceInitiator = serviceInitiatorForGrant(grant);
|
|
164
|
+
const callerSessionId = grant.metadata?.["sessionId"];
|
|
165
|
+
const callerTurnId = grant.metadata?.["turnId"];
|
|
166
|
+
const callerAttemptId = grant.metadata?.["attemptId"];
|
|
167
|
+
const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
|
|
168
|
+
const hasCallerTurnClaim =
|
|
169
|
+
callerTurnId !== undefined ||
|
|
170
|
+
callerAttemptId !== undefined ||
|
|
171
|
+
callerExecutionGeneration !== undefined;
|
|
172
|
+
if (hasCallerTurnClaim) {
|
|
173
|
+
if (
|
|
174
|
+
typeof callerSessionId !== "string" ||
|
|
175
|
+
typeof callerTurnId !== "string" ||
|
|
176
|
+
typeof callerAttemptId !== "string" ||
|
|
177
|
+
typeof callerExecutionGeneration !== "number" ||
|
|
178
|
+
!Number.isSafeInteger(callerExecutionGeneration) ||
|
|
179
|
+
callerExecutionGeneration < 1
|
|
180
|
+
) {
|
|
181
|
+
throw new HTTPException(403, { message: "caller attempt claims are incomplete" });
|
|
182
|
+
}
|
|
183
|
+
const actor = {
|
|
184
|
+
type: "agent_attempt",
|
|
185
|
+
sessionId: callerSessionId,
|
|
186
|
+
turnId: callerTurnId,
|
|
187
|
+
attemptId: callerAttemptId,
|
|
188
|
+
executionGeneration: callerExecutionGeneration,
|
|
189
|
+
} as const;
|
|
190
|
+
// The DB create transaction validates this exact attempt and derives the
|
|
191
|
+
// inherited subject under the same locks as the child-session insert.
|
|
192
|
+
return { actor };
|
|
193
|
+
}
|
|
194
|
+
if (serviceInitiator) {
|
|
195
|
+
return serviceInitiator;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
initiator: {
|
|
199
|
+
kind: "subject",
|
|
200
|
+
subjectId: grant.subjectId,
|
|
201
|
+
...(grant.subjectLabel ? { label: grant.subjectLabel } : {}),
|
|
202
|
+
},
|
|
203
|
+
context: {},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
87
207
|
function normalizedSessionMcpCredentialHeaders(
|
|
88
208
|
headers: Record<string, string> | undefined,
|
|
89
209
|
): Record<string, string> {
|
|
@@ -133,6 +253,22 @@ function mcpServerConfigFromInput(server: SessionMcpServerInput): Settings["mcpS
|
|
|
133
253
|
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
134
254
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
135
255
|
...(server.requireApproval !== undefined ? { requireApproval: server.requireApproval } : {}),
|
|
256
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function mcpServerConfigFromStoredInput(
|
|
261
|
+
server: CreateSessionMcpServerInput,
|
|
262
|
+
): Settings["mcpServers"][number] {
|
|
263
|
+
return {
|
|
264
|
+
id: server.id,
|
|
265
|
+
...(server.name ? { name: server.name } : {}),
|
|
266
|
+
url: server.url,
|
|
267
|
+
...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
|
|
268
|
+
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
269
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
270
|
+
...(server.requireApproval != null ? { requireApproval: server.requireApproval } : {}),
|
|
271
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
136
272
|
};
|
|
137
273
|
}
|
|
138
274
|
|
|
@@ -144,6 +280,8 @@ function mcpServerConfigFromMetadata(
|
|
|
144
280
|
...(server.name ? { name: server.name } : {}),
|
|
145
281
|
url: server.url,
|
|
146
282
|
cacheToolsList: false,
|
|
283
|
+
requireApproval: server.requireApproval,
|
|
284
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
147
285
|
};
|
|
148
286
|
}
|
|
149
287
|
|
|
@@ -177,7 +315,9 @@ function validateSessionMcpServersForCreate(
|
|
|
177
315
|
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
178
316
|
}
|
|
179
317
|
requirePermission(grant, "mcp_servers:attach");
|
|
180
|
-
const encryptionKey =
|
|
318
|
+
const encryptionKey = servers.some((server) => Object.keys(server.headers ?? {}).length > 0)
|
|
319
|
+
? requireVariableSetEncryption(settings)
|
|
320
|
+
: null;
|
|
181
321
|
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
182
322
|
const seenIds = new Set<string>();
|
|
183
323
|
const runtimeServers: Settings["mcpServers"] = [];
|
|
@@ -195,7 +335,7 @@ function validateSessionMcpServersForCreate(
|
|
|
195
335
|
const headersEncrypted = Object.fromEntries(
|
|
196
336
|
Object.entries(headers).map(([name, value]) => [
|
|
197
337
|
name,
|
|
198
|
-
encryptVariableSetValue(encryptionKey
|
|
338
|
+
encryptVariableSetValue(encryptionKey!, value),
|
|
199
339
|
]),
|
|
200
340
|
);
|
|
201
341
|
runtimeServers.push(mcpServerConfigFromInput(server));
|
|
@@ -207,6 +347,7 @@ function validateSessionMcpServersForCreate(
|
|
|
207
347
|
timeoutMs: server.timeoutMs ?? null,
|
|
208
348
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
209
349
|
requireApproval: server.requireApproval ?? null,
|
|
350
|
+
connectionRef: server.connectionRef ?? null,
|
|
210
351
|
headersEncrypted,
|
|
211
352
|
});
|
|
212
353
|
metadata.push({
|
|
@@ -215,11 +356,56 @@ function validateSessionMcpServersForCreate(
|
|
|
215
356
|
url: server.url,
|
|
216
357
|
headerNames: Object.keys(headersEncrypted).sort(),
|
|
217
358
|
credentialVersion: 1,
|
|
359
|
+
requireApproval: server.requireApproval ?? false,
|
|
360
|
+
connectionRef: server.connectionRef ?? null,
|
|
218
361
|
});
|
|
219
362
|
}
|
|
220
363
|
return { runtimeServers, dbServers, metadata };
|
|
221
364
|
}
|
|
222
365
|
|
|
366
|
+
function validateInheritedSessionMcpServersForCreate(
|
|
367
|
+
servers: CreateSessionMcpServerInput[],
|
|
368
|
+
): ValidatedSessionMcpServers {
|
|
369
|
+
if (servers.length === 0) {
|
|
370
|
+
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
371
|
+
}
|
|
372
|
+
const seenIds = new Set<string>();
|
|
373
|
+
for (const server of servers) {
|
|
374
|
+
if (seenIds.has(server.id)) {
|
|
375
|
+
throw new HTTPException(422, {
|
|
376
|
+
message: `duplicate inherited session MCP server id: ${server.id}`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
seenIds.add(server.id);
|
|
380
|
+
if (reservedSessionMcpServerIds.has(server.id)) {
|
|
381
|
+
throw new HTTPException(422, {
|
|
382
|
+
message: `reserved inherited session MCP server id: ${server.id}`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// A newly enabled deployment/workspace capability may now reuse an id that
|
|
387
|
+
// belonged to this parent attachment first. Preserve the parent's existing
|
|
388
|
+
// session-overlay precedence instead of making child creation depend on a
|
|
389
|
+
// later workspace setting; settingsWithSessionMcpServerConfigs performs that
|
|
390
|
+
// same overlay for ordinary parent turns.
|
|
391
|
+
return {
|
|
392
|
+
runtimeServers: servers.map(mcpServerConfigFromStoredInput),
|
|
393
|
+
dbServers: servers.map((server) => ({
|
|
394
|
+
...server,
|
|
395
|
+
headersEncrypted: { ...(server.headersEncrypted ?? {}) },
|
|
396
|
+
})),
|
|
397
|
+
metadata: servers.map((server) => ({
|
|
398
|
+
id: server.id,
|
|
399
|
+
name: server.name ?? null,
|
|
400
|
+
url: server.url,
|
|
401
|
+
headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
|
|
402
|
+
credentialVersion: 1,
|
|
403
|
+
requireApproval: server.requireApproval ?? false,
|
|
404
|
+
connectionRef: server.connectionRef ?? null,
|
|
405
|
+
})),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
223
409
|
function validateSessionMcpCredentialUpdates(input: {
|
|
224
410
|
settings: Settings;
|
|
225
411
|
grant: AccessGrant;
|
|
@@ -258,19 +444,29 @@ function validateSessionMcpCredentialUpdates(input: {
|
|
|
258
444
|
}
|
|
259
445
|
|
|
260
446
|
export async function createAndStartSession(input: {
|
|
447
|
+
requestedSessionId?: string;
|
|
261
448
|
db: Database;
|
|
262
449
|
bus: EventBus;
|
|
263
450
|
workflowClient: SessionWorkflowClient;
|
|
264
451
|
accountId: string;
|
|
265
452
|
workspaceId: string;
|
|
266
453
|
initialMessage: string;
|
|
454
|
+
turnInstructions?: string | null;
|
|
267
455
|
resources: ResourceRef[];
|
|
268
456
|
tools: ToolRef[];
|
|
457
|
+
// Public admission always supplies provenance; optional keeps internal
|
|
458
|
+
// callers that predate durable tool-policy provenance source-compatible
|
|
459
|
+
// during the rolling deploy.
|
|
460
|
+
toolPolicy?: SessionToolPolicy;
|
|
269
461
|
clientEventId?: string;
|
|
270
462
|
model: string;
|
|
271
463
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
464
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
272
465
|
sandboxBackend: Settings["sandboxBackend"];
|
|
273
466
|
metadata: Record<string, unknown>;
|
|
467
|
+
createdBy?: TurnInitiator;
|
|
468
|
+
createdByContext?: TurnInitiatorContext;
|
|
469
|
+
createdByActor?: Extract<SessionCommandActor, { type: "agent_attempt" }> | null;
|
|
274
470
|
// Names/ids only; the session.created payload never carries variable values.
|
|
275
471
|
variableSet?: { id: string; name: string } | null;
|
|
276
472
|
// The rig + frozen active rig version resolved at create (M3). Both null ⇒ a
|
|
@@ -317,7 +513,7 @@ export async function createAndStartSession(input: {
|
|
|
317
513
|
// `workingDir` (optional) is the path/cwd base the chosen machine runs under,
|
|
318
514
|
// seeded alongside the pointer through the epoch-fenced CAS.
|
|
319
515
|
seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
|
|
320
|
-
}) {
|
|
516
|
+
}): Promise<CreateSessionResponse> {
|
|
321
517
|
const sessionMetadata = {
|
|
322
518
|
...input.metadata,
|
|
323
519
|
model: input.model,
|
|
@@ -332,6 +528,9 @@ export async function createAndStartSession(input: {
|
|
|
332
528
|
input.createIdempotencyKey,
|
|
333
529
|
);
|
|
334
530
|
if (existing) {
|
|
531
|
+
if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
|
|
532
|
+
throw new SessionIdConflictError(input.requestedSessionId);
|
|
533
|
+
}
|
|
335
534
|
return await finishStartSession(
|
|
336
535
|
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
337
536
|
existing,
|
|
@@ -344,12 +543,18 @@ export async function createAndStartSession(input: {
|
|
|
344
543
|
// advances the coalesced wake revision so an in-flight stale delivery can
|
|
345
544
|
// never acknowledge work committed by the other caller.
|
|
346
545
|
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
546
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
347
547
|
accountId: input.accountId,
|
|
348
548
|
workspaceId: input.workspaceId,
|
|
349
549
|
initialMessage: input.initialMessage,
|
|
550
|
+
initialTurnInstructions: input.turnInstructions ?? null,
|
|
350
551
|
resources: input.resources,
|
|
351
552
|
tools: input.tools,
|
|
553
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
352
554
|
metadata: sessionMetadata,
|
|
555
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
556
|
+
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
557
|
+
createdByActor: input.createdByActor ?? null,
|
|
353
558
|
model: input.model,
|
|
354
559
|
sandboxBackend: input.sandboxBackend,
|
|
355
560
|
variableSetId: input.variableSet?.id ?? null,
|
|
@@ -372,12 +577,18 @@ export async function createAndStartSession(input: {
|
|
|
372
577
|
return await finishStartSession(input, keyed);
|
|
373
578
|
}
|
|
374
579
|
const session = await createSession(input.db, {
|
|
580
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
375
581
|
accountId: input.accountId,
|
|
376
582
|
workspaceId: input.workspaceId,
|
|
377
583
|
initialMessage: input.initialMessage,
|
|
584
|
+
initialTurnInstructions: input.turnInstructions ?? null,
|
|
378
585
|
resources: input.resources,
|
|
379
586
|
tools: input.tools,
|
|
587
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
380
588
|
metadata: sessionMetadata,
|
|
589
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
590
|
+
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
591
|
+
createdByActor: input.createdByActor ?? null,
|
|
381
592
|
model: input.model,
|
|
382
593
|
sandboxBackend: input.sandboxBackend,
|
|
383
594
|
variableSetId: input.variableSet?.id ?? null,
|
|
@@ -405,11 +616,14 @@ async function finishStartSession(
|
|
|
405
616
|
bus: EventBus;
|
|
406
617
|
workflowClient: SessionWorkflowClient;
|
|
407
618
|
initialMessage: string;
|
|
619
|
+
turnInstructions?: string | null;
|
|
408
620
|
resources: ResourceRef[];
|
|
409
621
|
tools: ToolRef[];
|
|
622
|
+
toolPolicy?: SessionToolPolicy;
|
|
410
623
|
clientEventId?: string;
|
|
411
624
|
model: string;
|
|
412
625
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
626
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
413
627
|
sandboxBackend: Settings["sandboxBackend"];
|
|
414
628
|
variableSet?: { id: string; name: string } | null;
|
|
415
629
|
goal?: GoalSpec | null;
|
|
@@ -421,7 +635,7 @@ async function finishStartSession(
|
|
|
421
635
|
} | null;
|
|
422
636
|
},
|
|
423
637
|
session: Session,
|
|
424
|
-
): Promise<
|
|
638
|
+
): Promise<CreateSessionResponse> {
|
|
425
639
|
// Create-time machine targeting (A-2a): seed the active-sandbox pointer BEFORE
|
|
426
640
|
// the atomic initial turn transaction, so the FIRST turn routes to the chosen
|
|
427
641
|
// machine. swapActiveSandbox does
|
|
@@ -460,7 +674,9 @@ async function finishStartSession(
|
|
|
460
674
|
sessionId: session.id,
|
|
461
675
|
...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
|
|
462
676
|
reasoningEffortFallback: input.reasoningEffort,
|
|
677
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
463
678
|
createdEventPayload: {
|
|
679
|
+
...(input.toolPolicy ? { toolPolicy: input.toolPolicy } : {}),
|
|
464
680
|
...(input.variableSet
|
|
465
681
|
? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name }
|
|
466
682
|
: {}),
|
|
@@ -488,7 +704,12 @@ async function finishStartSession(
|
|
|
488
704
|
wakeRevision: started.workflowWakeRevision,
|
|
489
705
|
});
|
|
490
706
|
}
|
|
491
|
-
|
|
707
|
+
const persisted = await requireSession(input.db, session.workspaceId, session.id);
|
|
708
|
+
const initialTurnId =
|
|
709
|
+
started.turn?.id ??
|
|
710
|
+
(await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ??
|
|
711
|
+
null;
|
|
712
|
+
return { ...persisted, initialTurnId };
|
|
492
713
|
}
|
|
493
714
|
|
|
494
715
|
export function workflowIdForSession(sessionId: string): string {
|
|
@@ -511,12 +732,16 @@ export function workflowIdForSession(sessionId: string): string {
|
|
|
511
732
|
* later) and the MCP surfaces that share them validate identically and cannot
|
|
512
733
|
* drift.
|
|
513
734
|
*/
|
|
514
|
-
export function
|
|
735
|
+
export function canonicalConfiguredModel(
|
|
736
|
+
settings: Settings,
|
|
737
|
+
model: string | null | undefined,
|
|
738
|
+
): string | null | undefined {
|
|
515
739
|
if (model === null || model === undefined) {
|
|
516
|
-
return;
|
|
740
|
+
return model;
|
|
517
741
|
}
|
|
518
|
-
|
|
519
|
-
|
|
742
|
+
const canonicalModel = canonicalizeConfiguredModelId(settings, model);
|
|
743
|
+
if (configuredAllowedModels(settings).includes(canonicalModel)) {
|
|
744
|
+
return canonicalModel;
|
|
520
745
|
}
|
|
521
746
|
// Codex subscription models (codex/<slug>) are injected per-workspace by the
|
|
522
747
|
// worker overlay at turn time, so they are never in the deployment-global
|
|
@@ -524,12 +749,16 @@ export function assertConfiguredModel(settings: Settings, model: string | null |
|
|
|
524
749
|
// only surfaces them for a connected workspace, and the worker enforces the
|
|
525
750
|
// actual connection (an unconnected workspace fails the turn with a clear
|
|
526
751
|
// "no Codex subscription connected" error rather than a misleading 422 here).
|
|
527
|
-
if (settings.codexSubscriptionEnabled &&
|
|
528
|
-
return;
|
|
752
|
+
if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
753
|
+
return canonicalModel;
|
|
529
754
|
}
|
|
530
755
|
throw new HTTPException(422, { message: `model is not available: ${model}` });
|
|
531
756
|
}
|
|
532
757
|
|
|
758
|
+
export function assertConfiguredModel(settings: Settings, model: string | null | undefined): void {
|
|
759
|
+
canonicalConfiguredModel(settings, model);
|
|
760
|
+
}
|
|
761
|
+
|
|
533
762
|
/**
|
|
534
763
|
* Reject a model the WORKSPACE's model policy blocks, at the same choke points
|
|
535
764
|
* as assertConfiguredModel — a 422 at the edge instead of a queued turn the
|
|
@@ -550,18 +779,25 @@ export async function assertWorkspaceModelPolicyAllows(
|
|
|
550
779
|
if (model === null || model === undefined) {
|
|
551
780
|
return;
|
|
552
781
|
}
|
|
782
|
+
const canonicalModel = canonicalConfiguredModel(settings, model);
|
|
783
|
+
if (canonicalModel === null || canonicalModel === undefined) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
553
786
|
const policy = await getWorkspaceModelPolicy(db, workspaceId);
|
|
554
787
|
if (!policy) {
|
|
555
788
|
return;
|
|
556
789
|
}
|
|
557
|
-
const providerId = policyProviderIdForModel(settings,
|
|
558
|
-
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
790
|
+
const providerId = policyProviderIdForModel(settings, canonicalModel);
|
|
791
|
+
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
792
|
+
providerId,
|
|
793
|
+
modelId: canonicalModel,
|
|
794
|
+
});
|
|
559
795
|
if (!verdict.allowed) {
|
|
560
796
|
throw new HTTPException(422, {
|
|
561
797
|
message:
|
|
562
798
|
verdict.reason === "provider"
|
|
563
|
-
? `model "${
|
|
564
|
-
: `model "${
|
|
799
|
+
? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers`
|
|
800
|
+
: `model "${canonicalModel}" is not allowed by this workspace's model policy`,
|
|
565
801
|
});
|
|
566
802
|
}
|
|
567
803
|
}
|
|
@@ -607,8 +843,10 @@ export async function postUserMessageTurn(input: {
|
|
|
607
843
|
workspaceId: string;
|
|
608
844
|
sessionId: string;
|
|
609
845
|
text: string;
|
|
846
|
+
turnInstructions?: string | null;
|
|
610
847
|
resources: ResourceRef[];
|
|
611
848
|
tools: ToolRef[];
|
|
849
|
+
toolsProvided: boolean;
|
|
612
850
|
model?: string | null;
|
|
613
851
|
reasoningEffort?: Settings["openaiReasoningEffort"] | null;
|
|
614
852
|
clientEventId?: string;
|
|
@@ -616,11 +854,15 @@ export async function postUserMessageTurn(input: {
|
|
|
616
854
|
delivery?: "send" | "steer";
|
|
617
855
|
origin?: "human" | "operator";
|
|
618
856
|
actor?: string;
|
|
857
|
+
actorLabel?: string;
|
|
858
|
+
commandActor?: SessionCommandActor;
|
|
619
859
|
controlEtag?: string | null;
|
|
620
860
|
expectedDraftRevision?: number | null;
|
|
861
|
+
reasoningEffortFallback?: Settings["openaiReasoningEffort"];
|
|
862
|
+
turnExecutionPolicy: TurnExecutionPolicyV1;
|
|
621
863
|
}): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
622
864
|
const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
|
|
623
|
-
const requestedModel = input.model ?? null;
|
|
865
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
624
866
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
625
867
|
// Reject an explicit per-message model the host does not expose; an omitted
|
|
626
868
|
// model inherits the session's model downstream (always a configured id).
|
|
@@ -636,17 +878,24 @@ export async function postUserMessageTurn(input: {
|
|
|
636
878
|
workspaceId,
|
|
637
879
|
sessionId,
|
|
638
880
|
subjectId: input.actor ?? accountId,
|
|
639
|
-
|
|
881
|
+
...(input.actorLabel ? { subjectLabel: input.actorLabel } : {}),
|
|
882
|
+
actor: input.commandActor ?? {
|
|
883
|
+
type: "human",
|
|
884
|
+
subjectId: input.actor ?? accountId,
|
|
885
|
+
},
|
|
640
886
|
operationKey,
|
|
641
887
|
delivery: input.delivery ?? "send",
|
|
642
888
|
controlEtag: input.controlEtag ?? null,
|
|
643
889
|
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
644
890
|
text: input.text,
|
|
891
|
+
turnInstructions: input.turnInstructions ?? null,
|
|
645
892
|
resources: input.resources,
|
|
646
893
|
tools: input.tools,
|
|
894
|
+
toolsProvided: input.toolsProvided,
|
|
647
895
|
model: requestedModel,
|
|
648
896
|
reasoningEffort: requestedReasoningEffort,
|
|
649
|
-
reasoningEffortFallback: settings.openaiReasoningEffort,
|
|
897
|
+
reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
898
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
650
899
|
source: input.origin === "operator" ? "api" : "user",
|
|
651
900
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
652
901
|
}),
|
|
@@ -718,8 +967,10 @@ export async function postUserMessageTurn(input: {
|
|
|
718
967
|
* Full create-session flow shared by `POST /sessions` and the first-party MCP
|
|
719
968
|
* `session_create` tool: payload validation, resource/tool/variableSet
|
|
720
969
|
* checks, usage limits, session start, and usage recording. `rawPayload` is
|
|
721
|
-
* the unparsed request body so absent-vs-empty
|
|
722
|
-
*
|
|
970
|
+
* the unparsed request body so absent-vs-empty execution-context fields keep
|
|
971
|
+
* their meaning: a child inherits omitted resources/tools/mcpServers from its
|
|
972
|
+
* trusted immediate parent, while explicit arrays (including []) win. A
|
|
973
|
+
* top-level create with omitted tools applies workspace-default capability MCPs.
|
|
723
974
|
*/
|
|
724
975
|
export async function createSessionForRequest(
|
|
725
976
|
deps: ApiRouteDeps,
|
|
@@ -729,32 +980,100 @@ export async function createSessionForRequest(
|
|
|
729
980
|
): Promise<Session> {
|
|
730
981
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
731
982
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
983
|
+
// Parent linkage and execution-context inheritance come ONLY from the
|
|
984
|
+
// worker-signed sessionId claim. A caller cannot nominate a parent in the
|
|
985
|
+
// payload, so inheriting an existing repository/tool/credential snapshot does
|
|
986
|
+
// not turn sessions:create into arbitrary cross-session read authority.
|
|
987
|
+
const parentSessionId =
|
|
988
|
+
typeof grant.metadata?.["sessionId"] === "string"
|
|
989
|
+
? (grant.metadata["sessionId"] as string)
|
|
990
|
+
: null;
|
|
991
|
+
if (parentSessionId) {
|
|
992
|
+
await requireSessionAuthorization(deps, grant, {
|
|
993
|
+
sessionId: parentSessionId,
|
|
994
|
+
operation: "session.child.create",
|
|
995
|
+
surface: "core",
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
const parentSession = parentSessionId ? await getSession(db, workspaceId, parentSessionId) : null;
|
|
999
|
+
if (parentSessionId && !parentSession) {
|
|
1000
|
+
throw new HTTPException(404, {
|
|
1001
|
+
message: `parent session not found in workspace: ${parentSessionId}`,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
732
1004
|
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
733
1005
|
db,
|
|
734
1006
|
workspaceId,
|
|
735
1007
|
settings,
|
|
736
1008
|
);
|
|
737
|
-
const sessionMcpServers =
|
|
738
|
-
capabilityRuntimeSettings,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1009
|
+
const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers")
|
|
1010
|
+
? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers)
|
|
1011
|
+
: parentSession
|
|
1012
|
+
? validateInheritedSessionMcpServersForCreate(
|
|
1013
|
+
await listSessionMcpServersForChildInheritance(db, workspaceId, parentSession.id),
|
|
1014
|
+
)
|
|
1015
|
+
: validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
|
|
742
1016
|
const runtimeSettings = settingsWithSessionMcpServerConfigs(
|
|
743
1017
|
capabilityRuntimeSettings,
|
|
744
1018
|
sessionMcpServers.runtimeServers,
|
|
745
1019
|
);
|
|
746
|
-
const resources = normalizeResources(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1020
|
+
const resources = normalizeResources(
|
|
1021
|
+
hasOwnProperty(rawPayload, "resources")
|
|
1022
|
+
? payload.resources
|
|
1023
|
+
: (parentSession?.resources ?? payload.resources),
|
|
1024
|
+
);
|
|
1025
|
+
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
1026
|
+
const requestedTools = validateToolRefs(
|
|
1027
|
+
toolsProvided ? payload.tools : (parentSession?.tools ?? payload.tools),
|
|
1028
|
+
runtimeSettings,
|
|
1029
|
+
);
|
|
1030
|
+
let selectedTools: ToolRef[];
|
|
1031
|
+
let toolPolicy: SessionToolPolicy;
|
|
1032
|
+
if (parentSession) {
|
|
1033
|
+
const parentTracksWorkspaceDefaults = parentSession.toolPolicy?.mode === "workspace_default";
|
|
1034
|
+
const parentEffective = withFirstPartyTools(
|
|
1035
|
+
parentTracksWorkspaceDefaults
|
|
1036
|
+
? withDefaultEnabledCapabilityMcpTools(
|
|
1037
|
+
availableToolRefs(parentSession.tools, runtimeSettings),
|
|
1038
|
+
settings,
|
|
1039
|
+
runtimeSettings,
|
|
1040
|
+
)
|
|
1041
|
+
: parentSession.tools,
|
|
1042
|
+
runtimeSettings,
|
|
1043
|
+
);
|
|
1044
|
+
if (toolsProvided) {
|
|
1045
|
+
assertToolRefsSubset(
|
|
1046
|
+
requestedTools,
|
|
1047
|
+
parentEffective,
|
|
1048
|
+
"child tools may only narrow the parent session tool policy",
|
|
1049
|
+
);
|
|
1050
|
+
selectedTools = requestedTools;
|
|
1051
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
|
|
1052
|
+
} else {
|
|
1053
|
+
selectedTools = parentEffective;
|
|
1054
|
+
toolPolicy = {
|
|
1055
|
+
mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
|
|
1056
|
+
inheritedFromSessionId: parentSession.id,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
} else if (toolsProvided) {
|
|
1060
|
+
selectedTools = requestedTools;
|
|
1061
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: null };
|
|
1062
|
+
} else {
|
|
1063
|
+
selectedTools = withDefaultEnabledCapabilityMcpTools(
|
|
1064
|
+
requestedTools,
|
|
1065
|
+
settings,
|
|
1066
|
+
capabilityRuntimeSettings,
|
|
1067
|
+
);
|
|
1068
|
+
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
1069
|
+
}
|
|
751
1070
|
// The first-party MCP server is attached to EVERY session. It hosts the
|
|
752
1071
|
// session's own metadata tool (set_session_title) + goal tools, and — only
|
|
753
1072
|
// when the grant carries the permission — the orchestration/variableSet/
|
|
754
1073
|
// github tools. Capability is gated per-tool by permission, never by whether
|
|
755
1074
|
// the server is attached, so a bare chat still gets titling while the
|
|
756
1075
|
// dangerous tools stay off by default.
|
|
757
|
-
const tools = withFirstPartyTools(
|
|
1076
|
+
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
758
1077
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
759
1078
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
760
1079
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
@@ -800,24 +1119,57 @@ export async function createSessionForRequest(
|
|
|
800
1119
|
frozenRigVersionId = rig.activeVersion.id;
|
|
801
1120
|
}
|
|
802
1121
|
}
|
|
803
|
-
|
|
1122
|
+
const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
|
|
1123
|
+
if (model === null || model === undefined) {
|
|
1124
|
+
throw new Error("effective session model unexpectedly resolved to null");
|
|
1125
|
+
}
|
|
804
1126
|
// Session creation persists the EFFECTIVE model — an omitted payload.model
|
|
805
1127
|
// stamps the deployment default onto the session — so the policy must vet
|
|
806
1128
|
// that effective value, not just explicit ones (a restricted workspace's
|
|
807
1129
|
// default-model session would otherwise be born blocked).
|
|
808
|
-
await assertWorkspaceModelPolicyAllows(
|
|
809
|
-
db,
|
|
810
|
-
settings,
|
|
811
|
-
workspaceId,
|
|
812
|
-
payload.model ?? settings.openaiModel,
|
|
813
|
-
);
|
|
814
|
-
const model = payload.model ?? settings.openaiModel;
|
|
1130
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
|
|
815
1131
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
1132
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1133
|
+
modelId: model,
|
|
1134
|
+
requestedModelId: payload.model ?? null,
|
|
1135
|
+
modelSource: payload.model === undefined ? "deployment" : "explicit",
|
|
1136
|
+
reasoningEffort,
|
|
1137
|
+
reasoningSource: payload.reasoningEffort === undefined ? "deployment" : "explicit",
|
|
1138
|
+
});
|
|
1139
|
+
// Parent linkage was resolved above, before context validation. A child with
|
|
1140
|
+
// no explicit permission override inherits the creating session's effective
|
|
1141
|
+
// grant instead of silently expanding to standalone worker defaults.
|
|
816
1142
|
// A session's first-party MCP token can carry a non-default permission set
|
|
817
1143
|
// (how an operator hands a manager-style session the orchestration tools),
|
|
818
1144
|
// but never one out-ranking its creator: every requested permission must be
|
|
819
|
-
// held by the creating grant.
|
|
820
|
-
|
|
1145
|
+
// held by the creating grant. A top-level omission keeps the deployment's
|
|
1146
|
+
// normal worker defaults. A child omission inherits its creator's exact
|
|
1147
|
+
// effective grant, preserving a host/operator's narrowed capability boundary
|
|
1148
|
+
// through the whole session tree.
|
|
1149
|
+
const parentFirstPartyMcpPermissions = parentSession
|
|
1150
|
+
? [...(parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS)]
|
|
1151
|
+
: null;
|
|
1152
|
+
if (
|
|
1153
|
+
parentFirstPartyMcpPermissions &&
|
|
1154
|
+
payload.firstPartyMcpPermissions?.some(
|
|
1155
|
+
(permission) => !hasPermission(parentFirstPartyMcpPermissions, permission),
|
|
1156
|
+
)
|
|
1157
|
+
) {
|
|
1158
|
+
throw new HTTPException(403, {
|
|
1159
|
+
message: "child first-party MCP permissions may only narrow the parent session grant",
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
// A worker-signed creator may itself carry less authority than its parent
|
|
1163
|
+
// session (for example a narrowly delegated spawn token). Inherit the
|
|
1164
|
+
// intersection in the shared canonical default order so null/default parent
|
|
1165
|
+
// policies cannot expand when runtime signing resolves them.
|
|
1166
|
+
let firstPartyMcpPermissions =
|
|
1167
|
+
payload.firstPartyMcpPermissions ??
|
|
1168
|
+
(parentFirstPartyMcpPermissions
|
|
1169
|
+
? parentFirstPartyMcpPermissions.filter((permission) =>
|
|
1170
|
+
hasPermission(grant.permissions, permission),
|
|
1171
|
+
)
|
|
1172
|
+
: null);
|
|
821
1173
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
822
1174
|
// An empty set would sign an unusable zero-permission token; the default
|
|
823
1175
|
// worker set is expressed by omitting the field.
|
|
@@ -833,21 +1185,21 @@ export async function createSessionForRequest(
|
|
|
833
1185
|
});
|
|
834
1186
|
}
|
|
835
1187
|
}
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
// spawned session itself via the worker-signed sessionId claim, so a
|
|
843
|
-
// worker managing its OWN goal is not an escalation of the spawner's
|
|
844
|
-
// authority.
|
|
1188
|
+
// A goal-bearing session with an explicit/effective permission set must
|
|
1189
|
+
// already carry goals:manage. Without it the worker cannot stop its own
|
|
1190
|
+
// continuation loop, but silently adding it would violate the child
|
|
1191
|
+
// authority contract: a child inherits or narrows its creator's exact grant
|
|
1192
|
+
// and never gains an unrequested permission. Top-level omission remains the
|
|
1193
|
+
// deployment's worker default, which includes the goal tools.
|
|
845
1194
|
if (
|
|
846
1195
|
payload.goal &&
|
|
847
1196
|
firstPartyMcpPermissions &&
|
|
848
1197
|
!firstPartyMcpPermissions.includes("goals:manage")
|
|
849
1198
|
) {
|
|
850
|
-
|
|
1199
|
+
throw new HTTPException(422, {
|
|
1200
|
+
message:
|
|
1201
|
+
"goal-bearing sessions require goals:manage in the resulting first-party MCP permission set",
|
|
1202
|
+
});
|
|
851
1203
|
}
|
|
852
1204
|
// Parent linkage: a worker is linked to its manager ONLY from the
|
|
853
1205
|
// worker-signed sessionId claim on the creating grant — the manager
|
|
@@ -860,10 +1212,6 @@ export async function createSessionForRequest(
|
|
|
860
1212
|
// its completion wake injects a user.message + queued turn into that session
|
|
861
1213
|
// without holding sessions:control on it (a cross-session write escalation).
|
|
862
1214
|
// The claim is the only trustworthy parent source.
|
|
863
|
-
const parentSessionId =
|
|
864
|
-
typeof grant.metadata?.["sessionId"] === "string"
|
|
865
|
-
? (grant.metadata["sessionId"] as string)
|
|
866
|
-
: null;
|
|
867
1215
|
// Shared-sandbox placement (addendum 05 §D.2/§D.3, decision I10/OD-S1).
|
|
868
1216
|
//
|
|
869
1217
|
// The DEFAULT rule is context-dependent and resolved server-side from the
|
|
@@ -921,12 +1269,10 @@ export async function createSessionForRequest(
|
|
|
921
1269
|
"sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create.",
|
|
922
1270
|
});
|
|
923
1271
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
throw new HTTPException(404, {
|
|
927
|
-
message: `parent session not found in workspace: ${parentSessionId}`,
|
|
928
|
-
});
|
|
1272
|
+
if (!parentSession) {
|
|
1273
|
+
throw new Error("trusted parent session was not resolved");
|
|
929
1274
|
}
|
|
1275
|
+
const parent = parentSession;
|
|
930
1276
|
const parentBoxed = parent.sandboxBackend !== "none";
|
|
931
1277
|
const variableSetMismatch =
|
|
932
1278
|
parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
|
|
@@ -1074,53 +1420,74 @@ export async function createSessionForRequest(
|
|
|
1074
1420
|
quantity: 1,
|
|
1075
1421
|
model,
|
|
1076
1422
|
});
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
:
|
|
1123
|
-
|
|
1423
|
+
const creationInitiator = creationInitiatorForGrant(grant);
|
|
1424
|
+
let session: CreateSessionResponse;
|
|
1425
|
+
try {
|
|
1426
|
+
session = await createAndStartSession({
|
|
1427
|
+
...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
|
|
1428
|
+
db,
|
|
1429
|
+
bus,
|
|
1430
|
+
workflowClient,
|
|
1431
|
+
accountId: grant.accountId,
|
|
1432
|
+
workspaceId,
|
|
1433
|
+
initialMessage: payload.initialMessage,
|
|
1434
|
+
turnInstructions: payload.turnInstructions ?? null,
|
|
1435
|
+
resources,
|
|
1436
|
+
tools,
|
|
1437
|
+
toolPolicy,
|
|
1438
|
+
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
1439
|
+
model,
|
|
1440
|
+
reasoningEffort,
|
|
1441
|
+
turnExecutionPolicy,
|
|
1442
|
+
// A shared spawn inherits the box's backend; a caller-supplied
|
|
1443
|
+
// sandboxBackend on a shared spawn is ignored (it is the same box). A
|
|
1444
|
+
// machine-targeted top-level create labels the home "selfhosted"
|
|
1445
|
+
// (machineHomeBackend), overriding the caller/deployment default so the row
|
|
1446
|
+
// matches where the session actually runs.
|
|
1447
|
+
sandboxBackend:
|
|
1448
|
+
inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
|
|
1449
|
+
// Mirror the backend relabel on the OS axis: only a machine-targeted
|
|
1450
|
+
// top-level create carries a derived OS; everything else is omitted and the
|
|
1451
|
+
// "linux" default holds (shared spawns keep the parent-box behavior).
|
|
1452
|
+
...(machineHomeOs ? { sandboxOs: machineHomeOs } : {}),
|
|
1453
|
+
sandboxGroupId,
|
|
1454
|
+
metadata: payload.metadata,
|
|
1455
|
+
...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
|
|
1456
|
+
...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
|
|
1457
|
+
createdByActor: creationInitiator.actor ?? null,
|
|
1458
|
+
variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
|
|
1459
|
+
// Frozen rig binding (M3): both null for a rig-less session (today's path).
|
|
1460
|
+
rigId: frozenRigId,
|
|
1461
|
+
rigVersionId: frozenRigVersionId,
|
|
1462
|
+
goal: payload.goal ?? null,
|
|
1463
|
+
// Per-session persona instructions (already trimmed/validated by the
|
|
1464
|
+
// contracts schema). Persisted on the row; composed system-level at turn
|
|
1465
|
+
// time. Not surfaced as an event.
|
|
1466
|
+
instructions: payload.instructions ?? null,
|
|
1467
|
+
firstPartyMcpPermissions,
|
|
1468
|
+
mcpServers: sessionMcpServers.dbServers,
|
|
1469
|
+
sessionMcpServers: sessionMcpServers.metadata,
|
|
1470
|
+
parentSessionId,
|
|
1471
|
+
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
1472
|
+
// Create-time machine targeting (A-2a): when a target sandbox is named, the
|
|
1473
|
+
// active-sandbox pointer is seeded race-free inside createAndStartSession
|
|
1474
|
+
// (after the row exists, before the first turn dispatches). Validation
|
|
1475
|
+
// (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
|
|
1476
|
+
seedTargetSandbox: payload.targetSandboxId
|
|
1477
|
+
? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null }
|
|
1478
|
+
: null,
|
|
1479
|
+
});
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
if (error instanceof AgentCommandAuthorityError) {
|
|
1482
|
+
throw new HTTPException(403, { message: error.message });
|
|
1483
|
+
}
|
|
1484
|
+
if (error instanceof SessionIdConflictError) {
|
|
1485
|
+
throw new HTTPException(409, {
|
|
1486
|
+
message: "requested session id is already in use",
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
throw error;
|
|
1490
|
+
}
|
|
1124
1491
|
await recordWorkspaceUsage(deps, {
|
|
1125
1492
|
accountId: grant.accountId,
|
|
1126
1493
|
workspaceId,
|
|
@@ -1130,6 +1497,10 @@ export async function createSessionForRequest(
|
|
|
1130
1497
|
unit: "run",
|
|
1131
1498
|
sourceResourceType: "session",
|
|
1132
1499
|
sourceResourceId: session.id,
|
|
1500
|
+
sessionId: session.id,
|
|
1501
|
+
initiator: session.createdBy,
|
|
1502
|
+
initiatorContext: session.createdByContext,
|
|
1503
|
+
origin: creationInitiator.actor ? "system" : "user",
|
|
1133
1504
|
idempotencyKey: `agent_run.created:${workspaceId}:${session.id}`,
|
|
1134
1505
|
});
|
|
1135
1506
|
return session;
|
|
@@ -1139,8 +1510,9 @@ export async function createSessionForRequest(
|
|
|
1139
1510
|
* Full accept-user-message flow shared by the `user.message` branch of
|
|
1140
1511
|
* `POST /sessions/:id/events` and the first-party MCP `session_send_message`
|
|
1141
1512
|
* tool: resource/tool validation, usage limits, the locked append + turn
|
|
1142
|
-
* enqueue, and usage recording. `toolsProvided: false`
|
|
1143
|
-
*
|
|
1513
|
+
* enqueue, and usage recording. `toolsProvided: false` durably preserves an
|
|
1514
|
+
* absent `tools` key so execution inherits the session policy; an explicit
|
|
1515
|
+
* empty array is a deliberate per-turn narrowing.
|
|
1144
1516
|
*/
|
|
1145
1517
|
export async function acceptSessionUserMessage(
|
|
1146
1518
|
deps: AcceptSessionUserMessageDependencies,
|
|
@@ -1149,6 +1521,7 @@ export async function acceptSessionUserMessage(
|
|
|
1149
1521
|
sessionId: string,
|
|
1150
1522
|
input: {
|
|
1151
1523
|
text: string;
|
|
1524
|
+
turnInstructions?: string | null;
|
|
1152
1525
|
resources?: ResourceRef[];
|
|
1153
1526
|
tools?: ToolRef[];
|
|
1154
1527
|
toolsProvided: boolean;
|
|
@@ -1162,7 +1535,18 @@ export async function acceptSessionUserMessage(
|
|
|
1162
1535
|
expectedDraftRevision?: number | null;
|
|
1163
1536
|
},
|
|
1164
1537
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1538
|
+
if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
|
|
1539
|
+
throw new HTTPException(503, {
|
|
1540
|
+
message:
|
|
1541
|
+
"explicit follow-up tool replacement is temporarily unavailable until provenance-aware turn workers finish rolling out; omit tools to inherit the session policy and retry",
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1165
1544
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1545
|
+
await requireSessionAuthorization(deps, grant, {
|
|
1546
|
+
sessionId,
|
|
1547
|
+
operation: input.delivery === "steer" ? "session.steer" : "session.append",
|
|
1548
|
+
surface: "core",
|
|
1549
|
+
});
|
|
1166
1550
|
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1167
1551
|
db,
|
|
1168
1552
|
workspaceId,
|
|
@@ -1172,21 +1556,55 @@ export async function acceptSessionUserMessage(
|
|
|
1172
1556
|
// turn's effective model (a follow-up turn inherits the session's model). A
|
|
1173
1557
|
// pure read with no side effects.
|
|
1174
1558
|
const existingSession = await requireSession(db, workspaceId, sessionId);
|
|
1559
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
1560
|
+
const effectiveModel =
|
|
1561
|
+
canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
1562
|
+
if (effectiveModel === null) {
|
|
1563
|
+
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
1564
|
+
}
|
|
1565
|
+
const sessionReasoningEffort = reasoningEffortForSession(
|
|
1566
|
+
existingSession.metadata,
|
|
1567
|
+
settings.openaiReasoningEffort,
|
|
1568
|
+
);
|
|
1569
|
+
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
1570
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
1571
|
+
modelId: effectiveModel,
|
|
1572
|
+
requestedModelId: input.model ?? null,
|
|
1573
|
+
modelSource: input.model == null ? "session" : "explicit",
|
|
1574
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
1575
|
+
reasoningSource: input.reasoningEffort == null ? "session" : "explicit",
|
|
1576
|
+
});
|
|
1175
1577
|
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
1176
1578
|
capabilityRuntimeSettings,
|
|
1177
1579
|
existingSession.mcpServers,
|
|
1178
1580
|
);
|
|
1179
1581
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1582
|
+
const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
|
|
1583
|
+
const sessionPolicyTools = withFirstPartyTools(
|
|
1584
|
+
tracksWorkspaceDefaults
|
|
1585
|
+
? withDefaultEnabledCapabilityMcpTools(
|
|
1586
|
+
availableToolRefs(existingSession.tools, runtimeSettings),
|
|
1587
|
+
settings,
|
|
1588
|
+
capabilityRuntimeSettings,
|
|
1589
|
+
)
|
|
1590
|
+
: existingSession.tools,
|
|
1591
|
+
runtimeSettings,
|
|
1592
|
+
);
|
|
1593
|
+
const validatedTools = input.toolsProvided
|
|
1594
|
+
? validateToolRefsForSessionPolicy({
|
|
1595
|
+
requested: input.tools ?? [],
|
|
1596
|
+
settings: runtimeSettings,
|
|
1597
|
+
allowedTools: sessionPolicyTools,
|
|
1598
|
+
message: "message tools may only narrow the session tool policy",
|
|
1599
|
+
})
|
|
1600
|
+
: [];
|
|
1601
|
+
const requestedTools = input.toolsProvided ? validatedTools : [];
|
|
1184
1602
|
await requireLimit(deps, {
|
|
1185
1603
|
accountId: grant.accountId,
|
|
1186
1604
|
workspaceId,
|
|
1187
1605
|
action: "agent_run:create",
|
|
1188
1606
|
quantity: 1,
|
|
1189
|
-
model:
|
|
1607
|
+
model: effectiveModel,
|
|
1190
1608
|
});
|
|
1191
1609
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
1192
1610
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
@@ -1202,6 +1620,7 @@ export async function acceptSessionUserMessage(
|
|
|
1202
1620
|
session: existingSession,
|
|
1203
1621
|
updates: input.mcpCredentialUpdates ?? [],
|
|
1204
1622
|
});
|
|
1623
|
+
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
1205
1624
|
const { accepted, turn } = await postUserMessageTurn({
|
|
1206
1625
|
db,
|
|
1207
1626
|
bus,
|
|
@@ -1211,14 +1630,31 @@ export async function acceptSessionUserMessage(
|
|
|
1211
1630
|
workspaceId,
|
|
1212
1631
|
sessionId,
|
|
1213
1632
|
text: input.text,
|
|
1633
|
+
turnInstructions: input.turnInstructions ?? null,
|
|
1214
1634
|
resources: requestedResources,
|
|
1215
1635
|
tools: requestedTools,
|
|
1636
|
+
toolsProvided: input.toolsProvided,
|
|
1216
1637
|
model: input.model ?? null,
|
|
1217
1638
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
1639
|
+
reasoningEffortFallback: sessionReasoningEffort,
|
|
1640
|
+
turnExecutionPolicy,
|
|
1218
1641
|
mcpCredentialUpdates,
|
|
1219
1642
|
delivery: input.delivery ?? "send",
|
|
1220
|
-
origin: input.origin ?? "human",
|
|
1643
|
+
origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
|
|
1221
1644
|
actor: grant.subjectId,
|
|
1645
|
+
...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
|
|
1646
|
+
...(delegatedServiceInitiator
|
|
1647
|
+
? {
|
|
1648
|
+
commandActor: {
|
|
1649
|
+
type: "service" as const,
|
|
1650
|
+
subjectId: delegatedServiceInitiator.initiator.subjectId,
|
|
1651
|
+
...(delegatedServiceInitiator.initiator.label
|
|
1652
|
+
? { subjectLabel: delegatedServiceInitiator.initiator.label }
|
|
1653
|
+
: {}),
|
|
1654
|
+
context: delegatedServiceInitiator.context,
|
|
1655
|
+
},
|
|
1656
|
+
}
|
|
1657
|
+
: {}),
|
|
1222
1658
|
...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
|
|
1223
1659
|
...(input.expectedDraftRevision !== undefined
|
|
1224
1660
|
? { expectedDraftRevision: input.expectedDraftRevision }
|
|
@@ -1234,6 +1670,11 @@ export async function acceptSessionUserMessage(
|
|
|
1234
1670
|
unit: "run",
|
|
1235
1671
|
sourceResourceType: "session_turn",
|
|
1236
1672
|
sourceResourceId: turn.id,
|
|
1673
|
+
sessionId,
|
|
1674
|
+
turnId: turn.id,
|
|
1675
|
+
initiator: turn.initiator,
|
|
1676
|
+
initiatorContext: turn.initiatorContext,
|
|
1677
|
+
origin: turn.source,
|
|
1237
1678
|
idempotencyKey: `agent_run.created:${workspaceId}:${turn.id}`,
|
|
1238
1679
|
});
|
|
1239
1680
|
return { accepted, turn };
|
|
@@ -1249,13 +1690,27 @@ export async function acceptSessionUserMessage(
|
|
|
1249
1690
|
* happened so callers can avoid double work.
|
|
1250
1691
|
*/
|
|
1251
1692
|
export async function updateSessionTitle(
|
|
1252
|
-
deps: {
|
|
1253
|
-
|
|
1693
|
+
deps: {
|
|
1694
|
+
db: Database;
|
|
1695
|
+
bus: EventBus;
|
|
1696
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
1697
|
+
},
|
|
1698
|
+
grant: AccessGrant,
|
|
1254
1699
|
sessionId: string,
|
|
1255
1700
|
title: string,
|
|
1256
1701
|
source: "user" | "agent",
|
|
1257
|
-
): Promise<{
|
|
1702
|
+
): Promise<{
|
|
1703
|
+
updated: boolean;
|
|
1704
|
+
title: string | null;
|
|
1705
|
+
relatedSessionAccess: "target" | "root";
|
|
1706
|
+
}> {
|
|
1258
1707
|
const { db, bus } = deps;
|
|
1708
|
+
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
1709
|
+
sessionId,
|
|
1710
|
+
operation: "session.title.write",
|
|
1711
|
+
surface: "core",
|
|
1712
|
+
});
|
|
1713
|
+
const workspaceId = grant.workspaceId;
|
|
1259
1714
|
const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
|
|
1260
1715
|
if (result.updated) {
|
|
1261
1716
|
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
@@ -1268,11 +1723,92 @@ export async function updateSessionTitle(
|
|
|
1268
1723
|
},
|
|
1269
1724
|
]);
|
|
1270
1725
|
}
|
|
1271
|
-
return
|
|
1726
|
+
return {
|
|
1727
|
+
...result,
|
|
1728
|
+
relatedSessionAccess: authorization?.relatedSessionAccess ?? "root",
|
|
1729
|
+
};
|
|
1272
1730
|
}
|
|
1273
1731
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1732
|
+
/**
|
|
1733
|
+
* Update one existing session MCP server's approval policy. The database
|
|
1734
|
+
* serializes this write with attempt claim under the session lock: an already
|
|
1735
|
+
* claimed attempt retains its immutable snapshot, while the next claim captures
|
|
1736
|
+
* this value. No attempt is cancelled, restarted, or reinterpreted.
|
|
1737
|
+
*/
|
|
1738
|
+
export async function updateSessionMcpApprovalPolicy(
|
|
1739
|
+
deps: {
|
|
1740
|
+
db: Database;
|
|
1741
|
+
bus: EventBus;
|
|
1742
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
1743
|
+
},
|
|
1744
|
+
grant: AccessGrant,
|
|
1745
|
+
sessionId: string,
|
|
1746
|
+
serverId: string,
|
|
1747
|
+
requireApproval: SessionMcpApprovalPolicy,
|
|
1748
|
+
): Promise<UpdateSessionMcpApprovalPolicyResponse> {
|
|
1749
|
+
const normalizedPolicy = SessionMcpApprovalPolicy.parse(requireApproval);
|
|
1750
|
+
await requireSessionAuthorization(deps, grant, {
|
|
1751
|
+
sessionId,
|
|
1752
|
+
operation: "session.mcp.approval_policy.write",
|
|
1753
|
+
surface: "core",
|
|
1754
|
+
});
|
|
1755
|
+
requirePermission(grant, "sessions:control");
|
|
1756
|
+
|
|
1757
|
+
const outcome: { server?: SessionMcpServerMetadata } = {};
|
|
1758
|
+
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
1759
|
+
deps.db,
|
|
1760
|
+
grant.workspaceId,
|
|
1761
|
+
sessionId,
|
|
1762
|
+
async (_session, context) => {
|
|
1763
|
+
const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
|
|
1764
|
+
if (!result.server) {
|
|
1765
|
+
throw new HTTPException(404, { message: "session MCP server not found" });
|
|
1766
|
+
}
|
|
1767
|
+
outcome.server = result.server;
|
|
1768
|
+
return {
|
|
1769
|
+
events: result.changed
|
|
1770
|
+
? [
|
|
1771
|
+
{
|
|
1772
|
+
type: "session.mcp.approval_policy.updated" as const,
|
|
1773
|
+
payload: {
|
|
1774
|
+
serverId,
|
|
1775
|
+
effectiveFrom: "next_attempt",
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1778
|
+
]
|
|
1779
|
+
: [],
|
|
1780
|
+
};
|
|
1781
|
+
},
|
|
1782
|
+
);
|
|
1783
|
+
const updatedServer = outcome.server;
|
|
1784
|
+
if (!updatedServer) {
|
|
1785
|
+
throw new Error("session MCP approval policy update returned no server");
|
|
1786
|
+
}
|
|
1787
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
1788
|
+
return {
|
|
1789
|
+
server: updatedServer,
|
|
1790
|
+
effectiveFrom: "next_attempt",
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
export async function readSessionLineage(
|
|
1795
|
+
deps: Pick<ApiRouteDeps, "db" | "sessionAuthorization">,
|
|
1796
|
+
grant: AccessGrant,
|
|
1797
|
+
sessionId: string,
|
|
1798
|
+
) {
|
|
1799
|
+
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
1800
|
+
sessionId,
|
|
1801
|
+
operation: "session.lineage.read",
|
|
1802
|
+
surface: "core",
|
|
1803
|
+
});
|
|
1804
|
+
if (authorization?.relatedSessionAccess === "target") {
|
|
1805
|
+
const session = await getSession(deps.db, grant.workspaceId, sessionId);
|
|
1806
|
+
if (!session) {
|
|
1807
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1808
|
+
}
|
|
1809
|
+
return { ancestors: [], children: [], truncated: false };
|
|
1810
|
+
}
|
|
1811
|
+
const lineage = await getSessionLineage(deps.db, grant.workspaceId, sessionId);
|
|
1276
1812
|
if (!lineage) {
|
|
1277
1813
|
throw new HTTPException(404, { message: "session not found" });
|
|
1278
1814
|
}
|