@opengeni/core 0.4.10 → 0.8.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 +103 -138
- package/dist/index.js +792 -154
- 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 +190 -38
- package/src/billing/limits.ts +20 -1
- package/src/dependencies.ts +26 -2
- package/src/domain/capabilities.ts +171 -3
- package/src/domain/resources.ts +89 -30
- package/src/domain/sessions.ts +409 -93
- package/src/index.ts +1 -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
|
@@ -2,9 +2,12 @@ import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
|
2
2
|
import { configuredAllowedModels, policyProviderIdForModel, type Settings } from "@opengeni/config";
|
|
3
3
|
import {
|
|
4
4
|
CreateSessionRequest,
|
|
5
|
+
ServiceTurnInitiator,
|
|
6
|
+
ServiceTurnInitiatorContext,
|
|
5
7
|
evaluateWorkspaceModelPolicy,
|
|
6
8
|
reasoningEffortForMetadata,
|
|
7
9
|
type AccessGrant,
|
|
10
|
+
type CreateSessionResponse,
|
|
8
11
|
type GoalSpec,
|
|
9
12
|
type Permission,
|
|
10
13
|
type ReasoningEffort,
|
|
@@ -14,8 +17,11 @@ import {
|
|
|
14
17
|
type SessionMcpCredentialUpdateInput,
|
|
15
18
|
type SessionMcpServerInput,
|
|
16
19
|
type SessionMcpServerMetadata,
|
|
20
|
+
type SessionAuthorizationPort,
|
|
17
21
|
type SessionTurn,
|
|
18
22
|
type ToolRef,
|
|
23
|
+
type TurnInitiator,
|
|
24
|
+
type TurnInitiatorContext,
|
|
19
25
|
} from "@opengeni/contracts";
|
|
20
26
|
import {
|
|
21
27
|
createSession,
|
|
@@ -29,6 +35,7 @@ import {
|
|
|
29
35
|
listDistinctRigVersionIdsInGroup,
|
|
30
36
|
getSandbox,
|
|
31
37
|
getSession,
|
|
38
|
+
SessionIdConflictError,
|
|
32
39
|
getSessionByCreateIdempotencyKey,
|
|
33
40
|
getSessionEvent,
|
|
34
41
|
getWorkspaceControlEvent,
|
|
@@ -36,6 +43,8 @@ import {
|
|
|
36
43
|
getSessionTurn,
|
|
37
44
|
getWorkspaceModelPolicy,
|
|
38
45
|
initializeSessionStartAtomically,
|
|
46
|
+
listSessionTurns,
|
|
47
|
+
listSessionMcpServersForChildInheritance,
|
|
39
48
|
requireSession,
|
|
40
49
|
submitHumanPromptInTransaction,
|
|
41
50
|
updateSessionTitle as updateSessionTitleRow,
|
|
@@ -44,7 +53,9 @@ import {
|
|
|
44
53
|
type Database,
|
|
45
54
|
type UpdateSessionMcpServerCredentialsInput,
|
|
46
55
|
QueueCommandConflictError,
|
|
56
|
+
AgentCommandAuthorityError,
|
|
47
57
|
SessionControlConflictError,
|
|
58
|
+
type SessionCommandActor,
|
|
48
59
|
} from "@opengeni/db";
|
|
49
60
|
import {
|
|
50
61
|
appendAndPublishEvents,
|
|
@@ -60,6 +71,7 @@ import type {
|
|
|
60
71
|
ApiRouteDeps,
|
|
61
72
|
SessionWorkflowClient,
|
|
62
73
|
} from "../dependencies";
|
|
74
|
+
import { requireSessionAuthorization } from "../session-authorization";
|
|
63
75
|
import { swapActiveSandbox, type FleetContext } from "../sandbox/fleet";
|
|
64
76
|
import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
|
|
65
77
|
import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
|
|
@@ -84,6 +96,99 @@ type ValidatedSessionMcpServers = {
|
|
|
84
96
|
metadata: SessionMcpServerMetadata[];
|
|
85
97
|
};
|
|
86
98
|
|
|
99
|
+
type FrozenCreationInitiator = {
|
|
100
|
+
initiator?: TurnInitiator;
|
|
101
|
+
context?: TurnInitiatorContext;
|
|
102
|
+
actor?: Extract<SessionCommandActor, { type: "agent_attempt" }>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function serviceInitiatorForGrant(grant: AccessGrant): {
|
|
106
|
+
initiator: ServiceTurnInitiator;
|
|
107
|
+
context: ServiceTurnInitiatorContext;
|
|
108
|
+
} | null {
|
|
109
|
+
if (!grant.serviceInitiator) {
|
|
110
|
+
if (grant.serviceInitiatorContext) {
|
|
111
|
+
throw new HTTPException(403, {
|
|
112
|
+
message: "service initiator context requires a signed service initiator",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const initiator = ServiceTurnInitiator.safeParse(grant.serviceInitiator);
|
|
118
|
+
if (!initiator.success) {
|
|
119
|
+
throw new HTTPException(403, {
|
|
120
|
+
message: "a delegated command initiator must be a bounded service principal",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const context = ServiceTurnInitiatorContext.safeParse(grant.serviceInitiatorContext ?? {});
|
|
124
|
+
if (!context.success) {
|
|
125
|
+
throw new HTTPException(403, {
|
|
126
|
+
message: "delegated service initiator context is invalid or reserved",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const callerTurnId = grant.metadata?.["turnId"];
|
|
130
|
+
const callerAttemptId = grant.metadata?.["attemptId"];
|
|
131
|
+
const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
|
|
132
|
+
if (
|
|
133
|
+
callerTurnId !== undefined ||
|
|
134
|
+
callerAttemptId !== undefined ||
|
|
135
|
+
callerExecutionGeneration !== undefined
|
|
136
|
+
) {
|
|
137
|
+
throw new HTTPException(403, {
|
|
138
|
+
message: "a service initiator cannot replace an exact agent-attempt initiator",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
initiator: initiator.data,
|
|
143
|
+
context: context.data,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function creationInitiatorForGrant(grant: AccessGrant): FrozenCreationInitiator {
|
|
148
|
+
const serviceInitiator = serviceInitiatorForGrant(grant);
|
|
149
|
+
const callerSessionId = grant.metadata?.["sessionId"];
|
|
150
|
+
const callerTurnId = grant.metadata?.["turnId"];
|
|
151
|
+
const callerAttemptId = grant.metadata?.["attemptId"];
|
|
152
|
+
const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
|
|
153
|
+
const hasCallerTurnClaim =
|
|
154
|
+
callerTurnId !== undefined ||
|
|
155
|
+
callerAttemptId !== undefined ||
|
|
156
|
+
callerExecutionGeneration !== undefined;
|
|
157
|
+
if (hasCallerTurnClaim) {
|
|
158
|
+
if (
|
|
159
|
+
typeof callerSessionId !== "string" ||
|
|
160
|
+
typeof callerTurnId !== "string" ||
|
|
161
|
+
typeof callerAttemptId !== "string" ||
|
|
162
|
+
typeof callerExecutionGeneration !== "number" ||
|
|
163
|
+
!Number.isSafeInteger(callerExecutionGeneration) ||
|
|
164
|
+
callerExecutionGeneration < 1
|
|
165
|
+
) {
|
|
166
|
+
throw new HTTPException(403, { message: "caller attempt claims are incomplete" });
|
|
167
|
+
}
|
|
168
|
+
const actor = {
|
|
169
|
+
type: "agent_attempt",
|
|
170
|
+
sessionId: callerSessionId,
|
|
171
|
+
turnId: callerTurnId,
|
|
172
|
+
attemptId: callerAttemptId,
|
|
173
|
+
executionGeneration: callerExecutionGeneration,
|
|
174
|
+
} as const;
|
|
175
|
+
// The DB create transaction validates this exact attempt and derives the
|
|
176
|
+
// inherited subject under the same locks as the child-session insert.
|
|
177
|
+
return { actor };
|
|
178
|
+
}
|
|
179
|
+
if (serviceInitiator) {
|
|
180
|
+
return serviceInitiator;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
initiator: {
|
|
184
|
+
kind: "subject",
|
|
185
|
+
subjectId: grant.subjectId,
|
|
186
|
+
...(grant.subjectLabel ? { label: grant.subjectLabel } : {}),
|
|
187
|
+
},
|
|
188
|
+
context: {},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
87
192
|
function normalizedSessionMcpCredentialHeaders(
|
|
88
193
|
headers: Record<string, string> | undefined,
|
|
89
194
|
): Record<string, string> {
|
|
@@ -133,6 +238,22 @@ function mcpServerConfigFromInput(server: SessionMcpServerInput): Settings["mcpS
|
|
|
133
238
|
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
134
239
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
135
240
|
...(server.requireApproval !== undefined ? { requireApproval: server.requireApproval } : {}),
|
|
241
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function mcpServerConfigFromStoredInput(
|
|
246
|
+
server: CreateSessionMcpServerInput,
|
|
247
|
+
): Settings["mcpServers"][number] {
|
|
248
|
+
return {
|
|
249
|
+
id: server.id,
|
|
250
|
+
...(server.name ? { name: server.name } : {}),
|
|
251
|
+
url: server.url,
|
|
252
|
+
...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
|
|
253
|
+
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
254
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
255
|
+
...(server.requireApproval != null ? { requireApproval: server.requireApproval } : {}),
|
|
256
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
136
257
|
};
|
|
137
258
|
}
|
|
138
259
|
|
|
@@ -144,6 +265,7 @@ function mcpServerConfigFromMetadata(
|
|
|
144
265
|
...(server.name ? { name: server.name } : {}),
|
|
145
266
|
url: server.url,
|
|
146
267
|
cacheToolsList: false,
|
|
268
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
147
269
|
};
|
|
148
270
|
}
|
|
149
271
|
|
|
@@ -177,7 +299,9 @@ function validateSessionMcpServersForCreate(
|
|
|
177
299
|
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
178
300
|
}
|
|
179
301
|
requirePermission(grant, "mcp_servers:attach");
|
|
180
|
-
const encryptionKey =
|
|
302
|
+
const encryptionKey = servers.some((server) => Object.keys(server.headers ?? {}).length > 0)
|
|
303
|
+
? requireVariableSetEncryption(settings)
|
|
304
|
+
: null;
|
|
181
305
|
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
182
306
|
const seenIds = new Set<string>();
|
|
183
307
|
const runtimeServers: Settings["mcpServers"] = [];
|
|
@@ -195,7 +319,7 @@ function validateSessionMcpServersForCreate(
|
|
|
195
319
|
const headersEncrypted = Object.fromEntries(
|
|
196
320
|
Object.entries(headers).map(([name, value]) => [
|
|
197
321
|
name,
|
|
198
|
-
encryptVariableSetValue(encryptionKey
|
|
322
|
+
encryptVariableSetValue(encryptionKey!, value),
|
|
199
323
|
]),
|
|
200
324
|
);
|
|
201
325
|
runtimeServers.push(mcpServerConfigFromInput(server));
|
|
@@ -207,6 +331,7 @@ function validateSessionMcpServersForCreate(
|
|
|
207
331
|
timeoutMs: server.timeoutMs ?? null,
|
|
208
332
|
cacheToolsList: server.cacheToolsList ?? false,
|
|
209
333
|
requireApproval: server.requireApproval ?? null,
|
|
334
|
+
connectionRef: server.connectionRef ?? null,
|
|
210
335
|
headersEncrypted,
|
|
211
336
|
});
|
|
212
337
|
metadata.push({
|
|
@@ -215,11 +340,54 @@ function validateSessionMcpServersForCreate(
|
|
|
215
340
|
url: server.url,
|
|
216
341
|
headerNames: Object.keys(headersEncrypted).sort(),
|
|
217
342
|
credentialVersion: 1,
|
|
343
|
+
connectionRef: server.connectionRef ?? null,
|
|
218
344
|
});
|
|
219
345
|
}
|
|
220
346
|
return { runtimeServers, dbServers, metadata };
|
|
221
347
|
}
|
|
222
348
|
|
|
349
|
+
function validateInheritedSessionMcpServersForCreate(
|
|
350
|
+
servers: CreateSessionMcpServerInput[],
|
|
351
|
+
): ValidatedSessionMcpServers {
|
|
352
|
+
if (servers.length === 0) {
|
|
353
|
+
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
354
|
+
}
|
|
355
|
+
const seenIds = new Set<string>();
|
|
356
|
+
for (const server of servers) {
|
|
357
|
+
if (seenIds.has(server.id)) {
|
|
358
|
+
throw new HTTPException(422, {
|
|
359
|
+
message: `duplicate inherited session MCP server id: ${server.id}`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
seenIds.add(server.id);
|
|
363
|
+
if (reservedSessionMcpServerIds.has(server.id)) {
|
|
364
|
+
throw new HTTPException(422, {
|
|
365
|
+
message: `reserved inherited session MCP server id: ${server.id}`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// A newly enabled deployment/workspace capability may now reuse an id that
|
|
370
|
+
// belonged to this parent attachment first. Preserve the parent's existing
|
|
371
|
+
// session-overlay precedence instead of making child creation depend on a
|
|
372
|
+
// later workspace setting; settingsWithSessionMcpServerConfigs performs that
|
|
373
|
+
// same overlay for ordinary parent turns.
|
|
374
|
+
return {
|
|
375
|
+
runtimeServers: servers.map(mcpServerConfigFromStoredInput),
|
|
376
|
+
dbServers: servers.map((server) => ({
|
|
377
|
+
...server,
|
|
378
|
+
headersEncrypted: { ...(server.headersEncrypted ?? {}) },
|
|
379
|
+
})),
|
|
380
|
+
metadata: servers.map((server) => ({
|
|
381
|
+
id: server.id,
|
|
382
|
+
name: server.name ?? null,
|
|
383
|
+
url: server.url,
|
|
384
|
+
headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
|
|
385
|
+
credentialVersion: 1,
|
|
386
|
+
connectionRef: server.connectionRef ?? null,
|
|
387
|
+
})),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
223
391
|
function validateSessionMcpCredentialUpdates(input: {
|
|
224
392
|
settings: Settings;
|
|
225
393
|
grant: AccessGrant;
|
|
@@ -258,12 +426,14 @@ function validateSessionMcpCredentialUpdates(input: {
|
|
|
258
426
|
}
|
|
259
427
|
|
|
260
428
|
export async function createAndStartSession(input: {
|
|
429
|
+
requestedSessionId?: string;
|
|
261
430
|
db: Database;
|
|
262
431
|
bus: EventBus;
|
|
263
432
|
workflowClient: SessionWorkflowClient;
|
|
264
433
|
accountId: string;
|
|
265
434
|
workspaceId: string;
|
|
266
435
|
initialMessage: string;
|
|
436
|
+
turnInstructions?: string | null;
|
|
267
437
|
resources: ResourceRef[];
|
|
268
438
|
tools: ToolRef[];
|
|
269
439
|
clientEventId?: string;
|
|
@@ -271,6 +441,9 @@ export async function createAndStartSession(input: {
|
|
|
271
441
|
reasoningEffort: Settings["openaiReasoningEffort"];
|
|
272
442
|
sandboxBackend: Settings["sandboxBackend"];
|
|
273
443
|
metadata: Record<string, unknown>;
|
|
444
|
+
createdBy?: TurnInitiator;
|
|
445
|
+
createdByContext?: TurnInitiatorContext;
|
|
446
|
+
createdByActor?: Extract<SessionCommandActor, { type: "agent_attempt" }> | null;
|
|
274
447
|
// Names/ids only; the session.created payload never carries variable values.
|
|
275
448
|
variableSet?: { id: string; name: string } | null;
|
|
276
449
|
// The rig + frozen active rig version resolved at create (M3). Both null ⇒ a
|
|
@@ -317,7 +490,7 @@ export async function createAndStartSession(input: {
|
|
|
317
490
|
// `workingDir` (optional) is the path/cwd base the chosen machine runs under,
|
|
318
491
|
// seeded alongside the pointer through the epoch-fenced CAS.
|
|
319
492
|
seedTargetSandbox?: { sandboxId: string; settings: Settings; workingDir?: string | null } | null;
|
|
320
|
-
}) {
|
|
493
|
+
}): Promise<CreateSessionResponse> {
|
|
321
494
|
const sessionMetadata = {
|
|
322
495
|
...input.metadata,
|
|
323
496
|
model: input.model,
|
|
@@ -332,6 +505,9 @@ export async function createAndStartSession(input: {
|
|
|
332
505
|
input.createIdempotencyKey,
|
|
333
506
|
);
|
|
334
507
|
if (existing) {
|
|
508
|
+
if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
|
|
509
|
+
throw new SessionIdConflictError(input.requestedSessionId);
|
|
510
|
+
}
|
|
335
511
|
return await finishStartSession(
|
|
336
512
|
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
337
513
|
existing,
|
|
@@ -344,12 +520,17 @@ export async function createAndStartSession(input: {
|
|
|
344
520
|
// advances the coalesced wake revision so an in-flight stale delivery can
|
|
345
521
|
// never acknowledge work committed by the other caller.
|
|
346
522
|
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
523
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
347
524
|
accountId: input.accountId,
|
|
348
525
|
workspaceId: input.workspaceId,
|
|
349
526
|
initialMessage: input.initialMessage,
|
|
527
|
+
initialTurnInstructions: input.turnInstructions ?? null,
|
|
350
528
|
resources: input.resources,
|
|
351
529
|
tools: input.tools,
|
|
352
530
|
metadata: sessionMetadata,
|
|
531
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
532
|
+
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
533
|
+
createdByActor: input.createdByActor ?? null,
|
|
353
534
|
model: input.model,
|
|
354
535
|
sandboxBackend: input.sandboxBackend,
|
|
355
536
|
variableSetId: input.variableSet?.id ?? null,
|
|
@@ -372,12 +553,17 @@ export async function createAndStartSession(input: {
|
|
|
372
553
|
return await finishStartSession(input, keyed);
|
|
373
554
|
}
|
|
374
555
|
const session = await createSession(input.db, {
|
|
556
|
+
...(input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {}),
|
|
375
557
|
accountId: input.accountId,
|
|
376
558
|
workspaceId: input.workspaceId,
|
|
377
559
|
initialMessage: input.initialMessage,
|
|
560
|
+
initialTurnInstructions: input.turnInstructions ?? null,
|
|
378
561
|
resources: input.resources,
|
|
379
562
|
tools: input.tools,
|
|
380
563
|
metadata: sessionMetadata,
|
|
564
|
+
...(input.createdBy ? { createdBy: input.createdBy } : {}),
|
|
565
|
+
...(input.createdByContext ? { createdByContext: input.createdByContext } : {}),
|
|
566
|
+
createdByActor: input.createdByActor ?? null,
|
|
381
567
|
model: input.model,
|
|
382
568
|
sandboxBackend: input.sandboxBackend,
|
|
383
569
|
variableSetId: input.variableSet?.id ?? null,
|
|
@@ -405,6 +591,7 @@ async function finishStartSession(
|
|
|
405
591
|
bus: EventBus;
|
|
406
592
|
workflowClient: SessionWorkflowClient;
|
|
407
593
|
initialMessage: string;
|
|
594
|
+
turnInstructions?: string | null;
|
|
408
595
|
resources: ResourceRef[];
|
|
409
596
|
tools: ToolRef[];
|
|
410
597
|
clientEventId?: string;
|
|
@@ -421,7 +608,7 @@ async function finishStartSession(
|
|
|
421
608
|
} | null;
|
|
422
609
|
},
|
|
423
610
|
session: Session,
|
|
424
|
-
): Promise<
|
|
611
|
+
): Promise<CreateSessionResponse> {
|
|
425
612
|
// Create-time machine targeting (A-2a): seed the active-sandbox pointer BEFORE
|
|
426
613
|
// the atomic initial turn transaction, so the FIRST turn routes to the chosen
|
|
427
614
|
// machine. swapActiveSandbox does
|
|
@@ -488,7 +675,12 @@ async function finishStartSession(
|
|
|
488
675
|
wakeRevision: started.workflowWakeRevision,
|
|
489
676
|
});
|
|
490
677
|
}
|
|
491
|
-
|
|
678
|
+
const persisted = await requireSession(input.db, session.workspaceId, session.id);
|
|
679
|
+
const initialTurnId =
|
|
680
|
+
started.turn?.id ??
|
|
681
|
+
(await listSessionTurns(input.db, session.workspaceId, session.id, 1))[0]?.id ??
|
|
682
|
+
null;
|
|
683
|
+
return { ...persisted, initialTurnId };
|
|
492
684
|
}
|
|
493
685
|
|
|
494
686
|
export function workflowIdForSession(sessionId: string): string {
|
|
@@ -607,6 +799,7 @@ export async function postUserMessageTurn(input: {
|
|
|
607
799
|
workspaceId: string;
|
|
608
800
|
sessionId: string;
|
|
609
801
|
text: string;
|
|
802
|
+
turnInstructions?: string | null;
|
|
610
803
|
resources: ResourceRef[];
|
|
611
804
|
tools: ToolRef[];
|
|
612
805
|
model?: string | null;
|
|
@@ -616,6 +809,8 @@ export async function postUserMessageTurn(input: {
|
|
|
616
809
|
delivery?: "send" | "steer";
|
|
617
810
|
origin?: "human" | "operator";
|
|
618
811
|
actor?: string;
|
|
812
|
+
actorLabel?: string;
|
|
813
|
+
commandActor?: SessionCommandActor;
|
|
619
814
|
controlEtag?: string | null;
|
|
620
815
|
expectedDraftRevision?: number | null;
|
|
621
816
|
}): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
@@ -636,12 +831,17 @@ export async function postUserMessageTurn(input: {
|
|
|
636
831
|
workspaceId,
|
|
637
832
|
sessionId,
|
|
638
833
|
subjectId: input.actor ?? accountId,
|
|
639
|
-
|
|
834
|
+
...(input.actorLabel ? { subjectLabel: input.actorLabel } : {}),
|
|
835
|
+
actor: input.commandActor ?? {
|
|
836
|
+
type: "human",
|
|
837
|
+
subjectId: input.actor ?? accountId,
|
|
838
|
+
},
|
|
640
839
|
operationKey,
|
|
641
840
|
delivery: input.delivery ?? "send",
|
|
642
841
|
controlEtag: input.controlEtag ?? null,
|
|
643
842
|
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
644
843
|
text: input.text,
|
|
844
|
+
turnInstructions: input.turnInstructions ?? null,
|
|
645
845
|
resources: input.resources,
|
|
646
846
|
tools: input.tools,
|
|
647
847
|
model: requestedModel,
|
|
@@ -718,8 +918,10 @@ export async function postUserMessageTurn(input: {
|
|
|
718
918
|
* Full create-session flow shared by `POST /sessions` and the first-party MCP
|
|
719
919
|
* `session_create` tool: payload validation, resource/tool/variableSet
|
|
720
920
|
* checks, usage limits, session start, and usage recording. `rawPayload` is
|
|
721
|
-
* the unparsed request body so absent-vs-empty
|
|
722
|
-
*
|
|
921
|
+
* the unparsed request body so absent-vs-empty execution-context fields keep
|
|
922
|
+
* their meaning: a child inherits omitted resources/tools/mcpServers from its
|
|
923
|
+
* trusted immediate parent, while explicit arrays (including []) win. A
|
|
924
|
+
* top-level create with omitted tools applies workspace-default capability MCPs.
|
|
723
925
|
*/
|
|
724
926
|
export async function createSessionForRequest(
|
|
725
927
|
deps: ApiRouteDeps,
|
|
@@ -729,25 +931,56 @@ export async function createSessionForRequest(
|
|
|
729
931
|
): Promise<Session> {
|
|
730
932
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
731
933
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
934
|
+
// Parent linkage and execution-context inheritance come ONLY from the
|
|
935
|
+
// worker-signed sessionId claim. A caller cannot nominate a parent in the
|
|
936
|
+
// payload, so inheriting an existing repository/tool/credential snapshot does
|
|
937
|
+
// not turn sessions:create into arbitrary cross-session read authority.
|
|
938
|
+
const parentSessionId =
|
|
939
|
+
typeof grant.metadata?.["sessionId"] === "string"
|
|
940
|
+
? (grant.metadata["sessionId"] as string)
|
|
941
|
+
: null;
|
|
942
|
+
if (parentSessionId) {
|
|
943
|
+
await requireSessionAuthorization(deps, grant, {
|
|
944
|
+
sessionId: parentSessionId,
|
|
945
|
+
operation: "session.child.create",
|
|
946
|
+
surface: "core",
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
const parentSession = parentSessionId ? await getSession(db, workspaceId, parentSessionId) : null;
|
|
950
|
+
if (parentSessionId && !parentSession) {
|
|
951
|
+
throw new HTTPException(404, {
|
|
952
|
+
message: `parent session not found in workspace: ${parentSessionId}`,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
732
955
|
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
733
956
|
db,
|
|
734
957
|
workspaceId,
|
|
735
958
|
settings,
|
|
736
959
|
);
|
|
737
|
-
const sessionMcpServers =
|
|
738
|
-
capabilityRuntimeSettings,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
960
|
+
const sessionMcpServers = hasOwnProperty(rawPayload, "mcpServers")
|
|
961
|
+
? validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers)
|
|
962
|
+
: parentSession
|
|
963
|
+
? validateInheritedSessionMcpServersForCreate(
|
|
964
|
+
await listSessionMcpServersForChildInheritance(db, workspaceId, parentSession.id),
|
|
965
|
+
)
|
|
966
|
+
: validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
|
|
742
967
|
const runtimeSettings = settingsWithSessionMcpServerConfigs(
|
|
743
968
|
capabilityRuntimeSettings,
|
|
744
969
|
sessionMcpServers.runtimeServers,
|
|
745
970
|
);
|
|
746
|
-
const resources = normalizeResources(
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
971
|
+
const resources = normalizeResources(
|
|
972
|
+
hasOwnProperty(rawPayload, "resources")
|
|
973
|
+
? payload.resources
|
|
974
|
+
: (parentSession?.resources ?? payload.resources),
|
|
975
|
+
);
|
|
976
|
+
const requestedTools = validateToolRefs(
|
|
977
|
+
hasOwnProperty(rawPayload, "tools") ? payload.tools : (parentSession?.tools ?? payload.tools),
|
|
978
|
+
runtimeSettings,
|
|
979
|
+
);
|
|
980
|
+
const defaultedTools =
|
|
981
|
+
hasOwnProperty(rawPayload, "tools") || parentSession
|
|
982
|
+
? requestedTools
|
|
983
|
+
: withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
|
|
751
984
|
// The first-party MCP server is attached to EVERY session. It hosts the
|
|
752
985
|
// session's own metadata tool (set_session_title) + goal tools, and — only
|
|
753
986
|
// when the grant carries the permission — the orchestration/variableSet/
|
|
@@ -813,11 +1046,18 @@ export async function createSessionForRequest(
|
|
|
813
1046
|
);
|
|
814
1047
|
const model = payload.model ?? settings.openaiModel;
|
|
815
1048
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
1049
|
+
// Parent linkage was resolved above, before context validation. A child with
|
|
1050
|
+
// no explicit permission override inherits the creating session's effective
|
|
1051
|
+
// grant instead of silently expanding to standalone worker defaults.
|
|
816
1052
|
// A session's first-party MCP token can carry a non-default permission set
|
|
817
1053
|
// (how an operator hands a manager-style session the orchestration tools),
|
|
818
1054
|
// but never one out-ranking its creator: every requested permission must be
|
|
819
|
-
// held by the creating grant.
|
|
820
|
-
|
|
1055
|
+
// held by the creating grant. A top-level omission keeps the deployment's
|
|
1056
|
+
// normal worker defaults. A child omission inherits its creator's exact
|
|
1057
|
+
// effective grant, preserving a host/operator's narrowed capability boundary
|
|
1058
|
+
// through the whole session tree.
|
|
1059
|
+
let firstPartyMcpPermissions =
|
|
1060
|
+
payload.firstPartyMcpPermissions ?? (parentSessionId ? [...new Set(grant.permissions)] : null);
|
|
821
1061
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
822
1062
|
// An empty set would sign an unusable zero-permission token; the default
|
|
823
1063
|
// worker set is expressed by omitting the field.
|
|
@@ -833,21 +1073,21 @@ export async function createSessionForRequest(
|
|
|
833
1073
|
});
|
|
834
1074
|
}
|
|
835
1075
|
}
|
|
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.
|
|
1076
|
+
// A goal-bearing session with an explicit/effective permission set must
|
|
1077
|
+
// already carry goals:manage. Without it the worker cannot stop its own
|
|
1078
|
+
// continuation loop, but silently adding it would violate the child
|
|
1079
|
+
// authority contract: a child inherits or narrows its creator's exact grant
|
|
1080
|
+
// and never gains an unrequested permission. Top-level omission remains the
|
|
1081
|
+
// deployment's worker default, which includes the goal tools.
|
|
845
1082
|
if (
|
|
846
1083
|
payload.goal &&
|
|
847
1084
|
firstPartyMcpPermissions &&
|
|
848
1085
|
!firstPartyMcpPermissions.includes("goals:manage")
|
|
849
1086
|
) {
|
|
850
|
-
|
|
1087
|
+
throw new HTTPException(422, {
|
|
1088
|
+
message:
|
|
1089
|
+
"goal-bearing sessions require goals:manage in the resulting first-party MCP permission set",
|
|
1090
|
+
});
|
|
851
1091
|
}
|
|
852
1092
|
// Parent linkage: a worker is linked to its manager ONLY from the
|
|
853
1093
|
// worker-signed sessionId claim on the creating grant — the manager
|
|
@@ -860,10 +1100,6 @@ export async function createSessionForRequest(
|
|
|
860
1100
|
// its completion wake injects a user.message + queued turn into that session
|
|
861
1101
|
// without holding sessions:control on it (a cross-session write escalation).
|
|
862
1102
|
// 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
1103
|
// Shared-sandbox placement (addendum 05 §D.2/§D.3, decision I10/OD-S1).
|
|
868
1104
|
//
|
|
869
1105
|
// The DEFAULT rule is context-dependent and resolved server-side from the
|
|
@@ -921,12 +1157,10 @@ export async function createSessionForRequest(
|
|
|
921
1157
|
"sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create.",
|
|
922
1158
|
});
|
|
923
1159
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
throw new HTTPException(404, {
|
|
927
|
-
message: `parent session not found in workspace: ${parentSessionId}`,
|
|
928
|
-
});
|
|
1160
|
+
if (!parentSession) {
|
|
1161
|
+
throw new Error("trusted parent session was not resolved");
|
|
929
1162
|
}
|
|
1163
|
+
const parent = parentSession;
|
|
930
1164
|
const parentBoxed = parent.sandboxBackend !== "none";
|
|
931
1165
|
const variableSetMismatch =
|
|
932
1166
|
parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
|
|
@@ -1074,53 +1308,72 @@ export async function createSessionForRequest(
|
|
|
1074
1308
|
quantity: 1,
|
|
1075
1309
|
model,
|
|
1076
1310
|
});
|
|
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
|
-
|
|
1311
|
+
const creationInitiator = creationInitiatorForGrant(grant);
|
|
1312
|
+
let session: CreateSessionResponse;
|
|
1313
|
+
try {
|
|
1314
|
+
session = await createAndStartSession({
|
|
1315
|
+
...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
|
|
1316
|
+
db,
|
|
1317
|
+
bus,
|
|
1318
|
+
workflowClient,
|
|
1319
|
+
accountId: grant.accountId,
|
|
1320
|
+
workspaceId,
|
|
1321
|
+
initialMessage: payload.initialMessage,
|
|
1322
|
+
turnInstructions: payload.turnInstructions ?? null,
|
|
1323
|
+
resources,
|
|
1324
|
+
tools,
|
|
1325
|
+
...(payload.clientEventId ? { clientEventId: payload.clientEventId } : {}),
|
|
1326
|
+
model,
|
|
1327
|
+
reasoningEffort,
|
|
1328
|
+
// A shared spawn inherits the box's backend; a caller-supplied
|
|
1329
|
+
// sandboxBackend on a shared spawn is ignored (it is the same box). A
|
|
1330
|
+
// machine-targeted top-level create labels the home "selfhosted"
|
|
1331
|
+
// (machineHomeBackend), overriding the caller/deployment default so the row
|
|
1332
|
+
// matches where the session actually runs.
|
|
1333
|
+
sandboxBackend:
|
|
1334
|
+
inheritedBackend ?? machineHomeBackend ?? payload.sandboxBackend ?? settings.sandboxBackend,
|
|
1335
|
+
// Mirror the backend relabel on the OS axis: only a machine-targeted
|
|
1336
|
+
// top-level create carries a derived OS; everything else is omitted and the
|
|
1337
|
+
// "linux" default holds (shared spawns keep the parent-box behavior).
|
|
1338
|
+
...(machineHomeOs ? { sandboxOs: machineHomeOs } : {}),
|
|
1339
|
+
sandboxGroupId,
|
|
1340
|
+
metadata: payload.metadata,
|
|
1341
|
+
...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
|
|
1342
|
+
...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
|
|
1343
|
+
createdByActor: creationInitiator.actor ?? null,
|
|
1344
|
+
variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
|
|
1345
|
+
// Frozen rig binding (M3): both null for a rig-less session (today's path).
|
|
1346
|
+
rigId: frozenRigId,
|
|
1347
|
+
rigVersionId: frozenRigVersionId,
|
|
1348
|
+
goal: payload.goal ?? null,
|
|
1349
|
+
// Per-session persona instructions (already trimmed/validated by the
|
|
1350
|
+
// contracts schema). Persisted on the row; composed system-level at turn
|
|
1351
|
+
// time. Not surfaced as an event.
|
|
1352
|
+
instructions: payload.instructions ?? null,
|
|
1353
|
+
firstPartyMcpPermissions,
|
|
1354
|
+
mcpServers: sessionMcpServers.dbServers,
|
|
1355
|
+
sessionMcpServers: sessionMcpServers.metadata,
|
|
1356
|
+
parentSessionId,
|
|
1357
|
+
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
1358
|
+
// Create-time machine targeting (A-2a): when a target sandbox is named, the
|
|
1359
|
+
// active-sandbox pointer is seeded race-free inside createAndStartSession
|
|
1360
|
+
// (after the row exists, before the first turn dispatches). Validation
|
|
1361
|
+
// (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
|
|
1362
|
+
seedTargetSandbox: payload.targetSandboxId
|
|
1363
|
+
? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null }
|
|
1364
|
+
: null,
|
|
1365
|
+
});
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
if (error instanceof AgentCommandAuthorityError) {
|
|
1368
|
+
throw new HTTPException(403, { message: error.message });
|
|
1369
|
+
}
|
|
1370
|
+
if (error instanceof SessionIdConflictError) {
|
|
1371
|
+
throw new HTTPException(409, {
|
|
1372
|
+
message: "requested session id is already in use",
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
throw error;
|
|
1376
|
+
}
|
|
1124
1377
|
await recordWorkspaceUsage(deps, {
|
|
1125
1378
|
accountId: grant.accountId,
|
|
1126
1379
|
workspaceId,
|
|
@@ -1130,6 +1383,10 @@ export async function createSessionForRequest(
|
|
|
1130
1383
|
unit: "run",
|
|
1131
1384
|
sourceResourceType: "session",
|
|
1132
1385
|
sourceResourceId: session.id,
|
|
1386
|
+
sessionId: session.id,
|
|
1387
|
+
initiator: session.createdBy,
|
|
1388
|
+
initiatorContext: session.createdByContext,
|
|
1389
|
+
origin: creationInitiator.actor ? "system" : "user",
|
|
1133
1390
|
idempotencyKey: `agent_run.created:${workspaceId}:${session.id}`,
|
|
1134
1391
|
});
|
|
1135
1392
|
return session;
|
|
@@ -1149,6 +1406,7 @@ export async function acceptSessionUserMessage(
|
|
|
1149
1406
|
sessionId: string,
|
|
1150
1407
|
input: {
|
|
1151
1408
|
text: string;
|
|
1409
|
+
turnInstructions?: string | null;
|
|
1152
1410
|
resources?: ResourceRef[];
|
|
1153
1411
|
tools?: ToolRef[];
|
|
1154
1412
|
toolsProvided: boolean;
|
|
@@ -1163,6 +1421,11 @@ export async function acceptSessionUserMessage(
|
|
|
1163
1421
|
},
|
|
1164
1422
|
): Promise<{ accepted: SessionEvent; turn: SessionTurn }> {
|
|
1165
1423
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
1424
|
+
await requireSessionAuthorization(deps, grant, {
|
|
1425
|
+
sessionId,
|
|
1426
|
+
operation: input.delivery === "steer" ? "session.steer" : "session.append",
|
|
1427
|
+
surface: "core",
|
|
1428
|
+
});
|
|
1166
1429
|
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
1167
1430
|
db,
|
|
1168
1431
|
workspaceId,
|
|
@@ -1202,6 +1465,7 @@ export async function acceptSessionUserMessage(
|
|
|
1202
1465
|
session: existingSession,
|
|
1203
1466
|
updates: input.mcpCredentialUpdates ?? [],
|
|
1204
1467
|
});
|
|
1468
|
+
const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
|
|
1205
1469
|
const { accepted, turn } = await postUserMessageTurn({
|
|
1206
1470
|
db,
|
|
1207
1471
|
bus,
|
|
@@ -1211,14 +1475,28 @@ export async function acceptSessionUserMessage(
|
|
|
1211
1475
|
workspaceId,
|
|
1212
1476
|
sessionId,
|
|
1213
1477
|
text: input.text,
|
|
1478
|
+
turnInstructions: input.turnInstructions ?? null,
|
|
1214
1479
|
resources: requestedResources,
|
|
1215
1480
|
tools: requestedTools,
|
|
1216
1481
|
model: input.model ?? null,
|
|
1217
1482
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
1218
1483
|
mcpCredentialUpdates,
|
|
1219
1484
|
delivery: input.delivery ?? "send",
|
|
1220
|
-
origin: input.origin ?? "human",
|
|
1485
|
+
origin: delegatedServiceInitiator ? "operator" : (input.origin ?? "human"),
|
|
1221
1486
|
actor: grant.subjectId,
|
|
1487
|
+
...(grant.subjectLabel ? { actorLabel: grant.subjectLabel } : {}),
|
|
1488
|
+
...(delegatedServiceInitiator
|
|
1489
|
+
? {
|
|
1490
|
+
commandActor: {
|
|
1491
|
+
type: "service" as const,
|
|
1492
|
+
subjectId: delegatedServiceInitiator.initiator.subjectId,
|
|
1493
|
+
...(delegatedServiceInitiator.initiator.label
|
|
1494
|
+
? { subjectLabel: delegatedServiceInitiator.initiator.label }
|
|
1495
|
+
: {}),
|
|
1496
|
+
context: delegatedServiceInitiator.context,
|
|
1497
|
+
},
|
|
1498
|
+
}
|
|
1499
|
+
: {}),
|
|
1222
1500
|
...(input.controlEtag !== undefined ? { controlEtag: input.controlEtag } : {}),
|
|
1223
1501
|
...(input.expectedDraftRevision !== undefined
|
|
1224
1502
|
? { expectedDraftRevision: input.expectedDraftRevision }
|
|
@@ -1234,6 +1512,11 @@ export async function acceptSessionUserMessage(
|
|
|
1234
1512
|
unit: "run",
|
|
1235
1513
|
sourceResourceType: "session_turn",
|
|
1236
1514
|
sourceResourceId: turn.id,
|
|
1515
|
+
sessionId,
|
|
1516
|
+
turnId: turn.id,
|
|
1517
|
+
initiator: turn.initiator,
|
|
1518
|
+
initiatorContext: turn.initiatorContext,
|
|
1519
|
+
origin: turn.source,
|
|
1237
1520
|
idempotencyKey: `agent_run.created:${workspaceId}:${turn.id}`,
|
|
1238
1521
|
});
|
|
1239
1522
|
return { accepted, turn };
|
|
@@ -1249,13 +1532,27 @@ export async function acceptSessionUserMessage(
|
|
|
1249
1532
|
* happened so callers can avoid double work.
|
|
1250
1533
|
*/
|
|
1251
1534
|
export async function updateSessionTitle(
|
|
1252
|
-
deps: {
|
|
1253
|
-
|
|
1535
|
+
deps: {
|
|
1536
|
+
db: Database;
|
|
1537
|
+
bus: EventBus;
|
|
1538
|
+
sessionAuthorization?: SessionAuthorizationPort | null;
|
|
1539
|
+
},
|
|
1540
|
+
grant: AccessGrant,
|
|
1254
1541
|
sessionId: string,
|
|
1255
1542
|
title: string,
|
|
1256
1543
|
source: "user" | "agent",
|
|
1257
|
-
): Promise<{
|
|
1544
|
+
): Promise<{
|
|
1545
|
+
updated: boolean;
|
|
1546
|
+
title: string | null;
|
|
1547
|
+
relatedSessionAccess: "target" | "root";
|
|
1548
|
+
}> {
|
|
1258
1549
|
const { db, bus } = deps;
|
|
1550
|
+
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
1551
|
+
sessionId,
|
|
1552
|
+
operation: "session.title.write",
|
|
1553
|
+
surface: "core",
|
|
1554
|
+
});
|
|
1555
|
+
const workspaceId = grant.workspaceId;
|
|
1259
1556
|
const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
|
|
1260
1557
|
if (result.updated) {
|
|
1261
1558
|
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
@@ -1268,11 +1565,30 @@ export async function updateSessionTitle(
|
|
|
1268
1565
|
},
|
|
1269
1566
|
]);
|
|
1270
1567
|
}
|
|
1271
|
-
return
|
|
1568
|
+
return {
|
|
1569
|
+
...result,
|
|
1570
|
+
relatedSessionAccess: authorization?.relatedSessionAccess ?? "root",
|
|
1571
|
+
};
|
|
1272
1572
|
}
|
|
1273
1573
|
|
|
1274
|
-
export async function readSessionLineage(
|
|
1275
|
-
|
|
1574
|
+
export async function readSessionLineage(
|
|
1575
|
+
deps: Pick<ApiRouteDeps, "db" | "sessionAuthorization">,
|
|
1576
|
+
grant: AccessGrant,
|
|
1577
|
+
sessionId: string,
|
|
1578
|
+
) {
|
|
1579
|
+
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
1580
|
+
sessionId,
|
|
1581
|
+
operation: "session.lineage.read",
|
|
1582
|
+
surface: "core",
|
|
1583
|
+
});
|
|
1584
|
+
if (authorization?.relatedSessionAccess === "target") {
|
|
1585
|
+
const session = await getSession(deps.db, grant.workspaceId, sessionId);
|
|
1586
|
+
if (!session) {
|
|
1587
|
+
throw new HTTPException(404, { message: "session not found" });
|
|
1588
|
+
}
|
|
1589
|
+
return { ancestors: [], children: [], truncated: false };
|
|
1590
|
+
}
|
|
1591
|
+
const lineage = await getSessionLineage(deps.db, grant.workspaceId, sessionId);
|
|
1276
1592
|
if (!lineage) {
|
|
1277
1593
|
throw new HTTPException(404, { message: "session not found" });
|
|
1278
1594
|
}
|