@opengeni/db 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-PSX56ZTL.js → chunk-T2U4H4Z2.js} +31 -1
- package/dist/chunk-T2U4H4Z2.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +293 -15
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +91 -9
- package/dist/{schema-fwrPBw5T.d.ts → schema-DuRsrmzD.d.ts} +269 -2
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -1
- package/drizzle/0035_session_mcp_servers.sql +50 -0
- package/drizzle/0036_modal_lease_orphan_reaper.sql +92 -0
- package/drizzle/0037_session_instructions.sql +18 -0
- package/package.json +3 -3
- package/src/event-payload-sanitizer.ts +58 -1
- package/src/index.ts +392 -21
- package/src/schema.ts +29 -0
- package/dist/chunk-PSX56ZTL.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
SessionGoal,
|
|
35
35
|
SessionGoalCreatedBy,
|
|
36
36
|
SessionGoalStatus,
|
|
37
|
+
SessionMcpServerMetadata,
|
|
37
38
|
SessionStatus,
|
|
38
39
|
SessionTurn,
|
|
39
40
|
SessionTurnSource,
|
|
@@ -348,6 +349,7 @@ export const allWorkspacePermissions: Permission[] = [
|
|
|
348
349
|
"api_keys:manage",
|
|
349
350
|
"environments:manage",
|
|
350
351
|
"environments:use",
|
|
352
|
+
"mcp_servers:attach",
|
|
351
353
|
"goals:manage",
|
|
352
354
|
"enrollments:read",
|
|
353
355
|
"enrollments:manage",
|
|
@@ -1200,6 +1202,33 @@ export type EnabledMcpCapabilityServer = {
|
|
|
1200
1202
|
headersEncrypted?: Record<string, string>;
|
|
1201
1203
|
};
|
|
1202
1204
|
|
|
1205
|
+
export type CreateSessionMcpServerInput = {
|
|
1206
|
+
id: string;
|
|
1207
|
+
name?: string | null;
|
|
1208
|
+
url: string;
|
|
1209
|
+
allowedTools?: string[] | null;
|
|
1210
|
+
timeoutMs?: number | null;
|
|
1211
|
+
cacheToolsList?: boolean | null;
|
|
1212
|
+
headersEncrypted?: Record<string, string>;
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
export type UpdateSessionMcpServerCredentialsInput = {
|
|
1216
|
+
id: string;
|
|
1217
|
+
headersEncrypted: Record<string, string>;
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
export type UpdateSessionMcpServerCredentialsResult = {
|
|
1221
|
+
servers: SessionMcpServerMetadata[];
|
|
1222
|
+
missingIds: string[];
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
export type SessionMcpServerForRun = SessionMcpServerMetadata & {
|
|
1226
|
+
allowedTools?: string[];
|
|
1227
|
+
timeoutMs?: number;
|
|
1228
|
+
cacheToolsList?: boolean;
|
|
1229
|
+
headers: Record<string, string>;
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1203
1232
|
export type EnqueueSessionTurnInput = {
|
|
1204
1233
|
accountId: string;
|
|
1205
1234
|
workspaceId: string;
|
|
@@ -3085,6 +3114,155 @@ function mapWorkspaceEnvironmentVariableMetadata(row: {
|
|
|
3085
3114
|
};
|
|
3086
3115
|
}
|
|
3087
3116
|
|
|
3117
|
+
function mapSessionMcpServerMetadata(row: typeof schema.sessionMcpServers.$inferSelect): SessionMcpServerMetadata {
|
|
3118
|
+
return {
|
|
3119
|
+
id: row.serverId,
|
|
3120
|
+
name: row.name ?? null,
|
|
3121
|
+
url: row.url,
|
|
3122
|
+
headerNames: Object.keys(row.headersEncrypted ?? {}).sort(),
|
|
3123
|
+
credentialVersion: Number(row.credentialVersion),
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
async function sessionMcpServerMetadataForSessions(
|
|
3128
|
+
db: Database,
|
|
3129
|
+
workspaceId: string,
|
|
3130
|
+
sessionIds: string[],
|
|
3131
|
+
): Promise<Map<string, SessionMcpServerMetadata[]>> {
|
|
3132
|
+
const grouped = new Map<string, SessionMcpServerMetadata[]>();
|
|
3133
|
+
if (sessionIds.length === 0) {
|
|
3134
|
+
return grouped;
|
|
3135
|
+
}
|
|
3136
|
+
const rows = await db.select().from(schema.sessionMcpServers)
|
|
3137
|
+
.where(and(
|
|
3138
|
+
eq(schema.sessionMcpServers.workspaceId, workspaceId),
|
|
3139
|
+
inArray(schema.sessionMcpServers.sessionId, sessionIds),
|
|
3140
|
+
))
|
|
3141
|
+
.orderBy(asc(schema.sessionMcpServers.createdAt), asc(schema.sessionMcpServers.serverId));
|
|
3142
|
+
for (const row of rows) {
|
|
3143
|
+
const list = grouped.get(row.sessionId) ?? [];
|
|
3144
|
+
list.push(mapSessionMcpServerMetadata(row));
|
|
3145
|
+
grouped.set(row.sessionId, list);
|
|
3146
|
+
}
|
|
3147
|
+
return grouped;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
async function insertSessionMcpServers(db: Database, input: {
|
|
3151
|
+
accountId: string;
|
|
3152
|
+
workspaceId: string;
|
|
3153
|
+
sessionId: string;
|
|
3154
|
+
servers: CreateSessionMcpServerInput[];
|
|
3155
|
+
}): Promise<SessionMcpServerMetadata[]> {
|
|
3156
|
+
if (input.servers.length === 0) {
|
|
3157
|
+
return [];
|
|
3158
|
+
}
|
|
3159
|
+
const rows = await db.insert(schema.sessionMcpServers).values(input.servers.map((server) => ({
|
|
3160
|
+
accountId: input.accountId,
|
|
3161
|
+
workspaceId: input.workspaceId,
|
|
3162
|
+
sessionId: input.sessionId,
|
|
3163
|
+
serverId: server.id,
|
|
3164
|
+
name: server.name ?? null,
|
|
3165
|
+
url: server.url,
|
|
3166
|
+
allowedTools: server.allowedTools ?? null,
|
|
3167
|
+
timeoutMs: server.timeoutMs ?? null,
|
|
3168
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
3169
|
+
headersEncrypted: server.headersEncrypted ?? {},
|
|
3170
|
+
}))).returning();
|
|
3171
|
+
return rows.map(mapSessionMcpServerMetadata);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
export async function createSessionMcpServers(db: Database, input: {
|
|
3175
|
+
accountId: string;
|
|
3176
|
+
workspaceId: string;
|
|
3177
|
+
sessionId: string;
|
|
3178
|
+
servers: CreateSessionMcpServerInput[];
|
|
3179
|
+
}): Promise<SessionMcpServerMetadata[]> {
|
|
3180
|
+
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) =>
|
|
3181
|
+
await insertSessionMcpServers(scopedDb, input)
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
export async function listSessionMcpServerMetadata(db: Database, workspaceId: string, sessionId: string): Promise<SessionMcpServerMetadata[]> {
|
|
3186
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3187
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [sessionId]);
|
|
3188
|
+
return grouped.get(sessionId) ?? [];
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
export async function updateSessionMcpServerCredentials(db: Database, input: {
|
|
3193
|
+
workspaceId: string;
|
|
3194
|
+
sessionId: string;
|
|
3195
|
+
updates: UpdateSessionMcpServerCredentialsInput[];
|
|
3196
|
+
}): Promise<UpdateSessionMcpServerCredentialsResult> {
|
|
3197
|
+
return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => await scopedDb.transaction(async (tx) =>
|
|
3198
|
+
await updateSessionMcpServerCredentialsInTransaction(tx, input)
|
|
3199
|
+
));
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
async function updateSessionMcpServerCredentialsInTransaction(
|
|
3203
|
+
tx: Pick<Database, "update">,
|
|
3204
|
+
input: {
|
|
3205
|
+
workspaceId: string;
|
|
3206
|
+
sessionId: string;
|
|
3207
|
+
updates: UpdateSessionMcpServerCredentialsInput[];
|
|
3208
|
+
},
|
|
3209
|
+
): Promise<UpdateSessionMcpServerCredentialsResult> {
|
|
3210
|
+
const servers: SessionMcpServerMetadata[] = [];
|
|
3211
|
+
const missingIds: string[] = [];
|
|
3212
|
+
for (const update of input.updates) {
|
|
3213
|
+
const [row] = await tx.update(schema.sessionMcpServers)
|
|
3214
|
+
.set({
|
|
3215
|
+
headersEncrypted: update.headersEncrypted,
|
|
3216
|
+
credentialVersion: sql`${schema.sessionMcpServers.credentialVersion} + 1`,
|
|
3217
|
+
updatedAt: new Date(),
|
|
3218
|
+
})
|
|
3219
|
+
.where(and(
|
|
3220
|
+
eq(schema.sessionMcpServers.workspaceId, input.workspaceId),
|
|
3221
|
+
eq(schema.sessionMcpServers.sessionId, input.sessionId),
|
|
3222
|
+
eq(schema.sessionMcpServers.serverId, update.id),
|
|
3223
|
+
))
|
|
3224
|
+
.returning();
|
|
3225
|
+
if (!row) {
|
|
3226
|
+
missingIds.push(update.id);
|
|
3227
|
+
} else {
|
|
3228
|
+
servers.push(mapSessionMcpServerMetadata(row));
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return { servers, missingIds };
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
export async function listSessionMcpServersForRun(
|
|
3235
|
+
db: Database,
|
|
3236
|
+
workspaceId: string,
|
|
3237
|
+
sessionId: string,
|
|
3238
|
+
encryptionKey: Uint8Array,
|
|
3239
|
+
): Promise<SessionMcpServerForRun[]> {
|
|
3240
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3241
|
+
const rows = await scopedDb.select().from(schema.sessionMcpServers)
|
|
3242
|
+
.where(and(
|
|
3243
|
+
eq(schema.sessionMcpServers.workspaceId, workspaceId),
|
|
3244
|
+
eq(schema.sessionMcpServers.sessionId, sessionId),
|
|
3245
|
+
))
|
|
3246
|
+
.orderBy(asc(schema.sessionMcpServers.createdAt), asc(schema.sessionMcpServers.serverId));
|
|
3247
|
+
return rows.map((row) => {
|
|
3248
|
+
let headers: Record<string, string>;
|
|
3249
|
+
try {
|
|
3250
|
+
headers = Object.fromEntries(Object.entries(row.headersEncrypted ?? {})
|
|
3251
|
+
.map(([name, stored]) => [name, decryptEnvironmentValue(encryptionKey, stored)]));
|
|
3252
|
+
} catch {
|
|
3253
|
+
throw new Error("session MCP server credential decryption failed");
|
|
3254
|
+
}
|
|
3255
|
+
return {
|
|
3256
|
+
...mapSessionMcpServerMetadata(row),
|
|
3257
|
+
...(row.allowedTools ? { allowedTools: row.allowedTools } : {}),
|
|
3258
|
+
...(row.timeoutMs ? { timeoutMs: row.timeoutMs } : {}),
|
|
3259
|
+
...(row.cacheToolsList ? { cacheToolsList: row.cacheToolsList } : {}),
|
|
3260
|
+
headers,
|
|
3261
|
+
};
|
|
3262
|
+
});
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3088
3266
|
export async function createSession(db: Database, input: {
|
|
3089
3267
|
accountId: string;
|
|
3090
3268
|
workspaceId: string;
|
|
@@ -3096,6 +3274,9 @@ export async function createSession(db: Database, input: {
|
|
|
3096
3274
|
sandboxBackend: SandboxBackend;
|
|
3097
3275
|
environmentId?: string | null;
|
|
3098
3276
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
3277
|
+
// Per-session agent persona/system instructions (org-visible, not a secret).
|
|
3278
|
+
// Null/omitted ⇒ the session carries none (composed instructions unchanged).
|
|
3279
|
+
instructions?: string | null;
|
|
3099
3280
|
parentSessionId?: string | null;
|
|
3100
3281
|
createIdempotencyKey?: string | null;
|
|
3101
3282
|
// The shared-sandbox group to join. Omit (or null) for a singleton group:
|
|
@@ -3103,6 +3284,7 @@ export async function createSession(db: Database, input: {
|
|
|
3103
3284
|
// shared spawn passes the parent's sandboxGroupId so both run in ONE box.
|
|
3104
3285
|
sandboxGroupId?: string | null;
|
|
3105
3286
|
sandboxOs?: SandboxOs;
|
|
3287
|
+
mcpServers?: CreateSessionMcpServerInput[];
|
|
3106
3288
|
}): Promise<Session> {
|
|
3107
3289
|
// Generate the id up front so the same uuid can seed sandbox_group_id for a
|
|
3108
3290
|
// singleton group (sandbox_group_id cannot SQL-default to id).
|
|
@@ -3122,6 +3304,7 @@ export async function createSession(db: Database, input: {
|
|
|
3122
3304
|
sandboxGroupId: input.sandboxGroupId ?? id,
|
|
3123
3305
|
environmentId: input.environmentId ?? null,
|
|
3124
3306
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
3307
|
+
instructions: input.instructions ?? null,
|
|
3125
3308
|
parentSessionId: input.parentSessionId ?? null,
|
|
3126
3309
|
createIdempotencyKey: input.createIdempotencyKey ?? null,
|
|
3127
3310
|
status: "queued",
|
|
@@ -3129,7 +3312,13 @@ export async function createSession(db: Database, input: {
|
|
|
3129
3312
|
if (!row) {
|
|
3130
3313
|
throw new Error("Failed to create session");
|
|
3131
3314
|
}
|
|
3132
|
-
|
|
3315
|
+
const mcpServers = await insertSessionMcpServers(scopedDb, {
|
|
3316
|
+
accountId: input.accountId,
|
|
3317
|
+
workspaceId: input.workspaceId,
|
|
3318
|
+
sessionId: row.id,
|
|
3319
|
+
servers: input.mcpServers ?? [],
|
|
3320
|
+
});
|
|
3321
|
+
return mapSession(row, mcpServers);
|
|
3133
3322
|
});
|
|
3134
3323
|
}
|
|
3135
3324
|
|
|
@@ -3154,12 +3343,15 @@ export async function createSessionWithIdempotencyKey(db: Database, input: {
|
|
|
3154
3343
|
sandboxBackend: SandboxBackend;
|
|
3155
3344
|
environmentId?: string | null;
|
|
3156
3345
|
firstPartyMcpPermissions?: Permission[] | null;
|
|
3346
|
+
// Per-session agent persona/system instructions (org-visible, not a secret).
|
|
3347
|
+
instructions?: string | null;
|
|
3157
3348
|
parentSessionId?: string | null;
|
|
3158
3349
|
createIdempotencyKey: string;
|
|
3159
3350
|
// The shared-sandbox group to join. Omit (or null) for a singleton group
|
|
3160
3351
|
// (group === the new row's own id); a shared spawn passes the parent's group.
|
|
3161
3352
|
sandboxGroupId?: string | null;
|
|
3162
3353
|
sandboxOs?: SandboxOs;
|
|
3354
|
+
mcpServers?: CreateSessionMcpServerInput[];
|
|
3163
3355
|
}): Promise<{ session: Session; created: boolean }> {
|
|
3164
3356
|
// Generate the id up front so the same uuid can seed sandbox_group_id for a
|
|
3165
3357
|
// singleton group (sandbox_group_id cannot SQL-default to id).
|
|
@@ -3179,6 +3371,7 @@ export async function createSessionWithIdempotencyKey(db: Database, input: {
|
|
|
3179
3371
|
sandboxGroupId: input.sandboxGroupId ?? id,
|
|
3180
3372
|
environmentId: input.environmentId ?? null,
|
|
3181
3373
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
3374
|
+
instructions: input.instructions ?? null,
|
|
3182
3375
|
parentSessionId: input.parentSessionId ?? null,
|
|
3183
3376
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
3184
3377
|
status: "queued",
|
|
@@ -3187,7 +3380,13 @@ export async function createSessionWithIdempotencyKey(db: Database, input: {
|
|
|
3187
3380
|
where: sql`${schema.sessions.createIdempotencyKey} is not null`,
|
|
3188
3381
|
}).returning();
|
|
3189
3382
|
if (inserted) {
|
|
3190
|
-
|
|
3383
|
+
const mcpServers = await insertSessionMcpServers(scopedDb, {
|
|
3384
|
+
accountId: input.accountId,
|
|
3385
|
+
workspaceId: input.workspaceId,
|
|
3386
|
+
sessionId: inserted.id,
|
|
3387
|
+
servers: input.mcpServers ?? [],
|
|
3388
|
+
});
|
|
3389
|
+
return { session: mapSession(inserted, mcpServers), created: true };
|
|
3191
3390
|
}
|
|
3192
3391
|
const [existing] = await scopedDb.select().from(schema.sessions).where(and(
|
|
3193
3392
|
eq(schema.sessions.workspaceId, input.workspaceId),
|
|
@@ -3199,7 +3398,8 @@ export async function createSessionWithIdempotencyKey(db: Database, input: {
|
|
|
3199
3398
|
// than silently returning a phantom.
|
|
3200
3399
|
throw new Error("Failed to create session under idempotency key");
|
|
3201
3400
|
}
|
|
3202
|
-
|
|
3401
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, input.workspaceId, [existing.id]);
|
|
3402
|
+
return { session: mapSession(existing, grouped.get(existing.id) ?? []), created: false };
|
|
3203
3403
|
});
|
|
3204
3404
|
}
|
|
3205
3405
|
|
|
@@ -3209,14 +3409,18 @@ export async function getSessionByCreateIdempotencyKey(db: Database, workspaceId
|
|
|
3209
3409
|
eq(schema.sessions.workspaceId, workspaceId),
|
|
3210
3410
|
eq(schema.sessions.createIdempotencyKey, createIdempotencyKey),
|
|
3211
3411
|
)).limit(1);
|
|
3212
|
-
|
|
3412
|
+
if (!row) return null;
|
|
3413
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [row.id]);
|
|
3414
|
+
return mapSession(row, grouped.get(row.id) ?? []);
|
|
3213
3415
|
});
|
|
3214
3416
|
}
|
|
3215
3417
|
|
|
3216
3418
|
export async function getSession(db: Database, workspaceId: string, sessionId: string): Promise<Session | null> {
|
|
3217
3419
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3218
3420
|
const [row] = await scopedDb.select().from(schema.sessions).where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId))).limit(1);
|
|
3219
|
-
|
|
3421
|
+
if (!row) return null;
|
|
3422
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, [row.id]);
|
|
3423
|
+
return mapSession(row, grouped.get(row.id) ?? []);
|
|
3220
3424
|
});
|
|
3221
3425
|
}
|
|
3222
3426
|
|
|
@@ -3262,7 +3466,8 @@ export async function listSessions(db: Database, workspaceId: string, limit = 50
|
|
|
3262
3466
|
.where(eq(schema.sessions.workspaceId, workspaceId))
|
|
3263
3467
|
.orderBy(desc(schema.sessions.createdAt), desc(schema.sessions.id))
|
|
3264
3468
|
.limit(limit);
|
|
3265
|
-
|
|
3469
|
+
const grouped = await sessionMcpServerMetadataForSessions(scopedDb, workspaceId, rows.map((row) => row.id));
|
|
3470
|
+
return rows.map((row) => mapSession(row, grouped.get(row.id) ?? []));
|
|
3266
3471
|
});
|
|
3267
3472
|
}
|
|
3268
3473
|
|
|
@@ -3294,16 +3499,63 @@ export async function requireSession(db: Database, workspaceId: string, sessionI
|
|
|
3294
3499
|
return session;
|
|
3295
3500
|
}
|
|
3296
3501
|
|
|
3297
|
-
export
|
|
3502
|
+
export type ListSessionEventsOptions = {
|
|
3503
|
+
after?: number;
|
|
3504
|
+
before?: number;
|
|
3505
|
+
limit?: number;
|
|
3506
|
+
};
|
|
3507
|
+
|
|
3508
|
+
const POSTGRES_INT_MAX = 2_147_483_647;
|
|
3509
|
+
|
|
3510
|
+
export async function listSessionEvents(db: Database, workspaceId: string, sessionId: string): Promise<SessionEvent[]>;
|
|
3511
|
+
export async function listSessionEvents(db: Database, workspaceId: string, sessionId: string, after: number, limit?: number): Promise<SessionEvent[]>;
|
|
3512
|
+
export async function listSessionEvents(db: Database, workspaceId: string, sessionId: string, options: ListSessionEventsOptions): Promise<SessionEvent[]>;
|
|
3513
|
+
export async function listSessionEvents(
|
|
3514
|
+
db: Database,
|
|
3515
|
+
workspaceId: string,
|
|
3516
|
+
sessionId: string,
|
|
3517
|
+
afterOrOptions: number | ListSessionEventsOptions = 0,
|
|
3518
|
+
legacyLimit = 500,
|
|
3519
|
+
): Promise<SessionEvent[]> {
|
|
3520
|
+
const options = typeof afterOrOptions === "number"
|
|
3521
|
+
? { after: afterOrOptions, limit: legacyLimit }
|
|
3522
|
+
: afterOrOptions;
|
|
3523
|
+
const after = normalizeEventSequence(options.after, 0);
|
|
3524
|
+
const limit = normalizeEventLimit(options.limit, 500);
|
|
3525
|
+
const hasBefore = options.before !== undefined && Number.isFinite(options.before);
|
|
3526
|
+
const before = hasBefore ? Math.floor(options.before as number) : undefined;
|
|
3527
|
+
|
|
3298
3528
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3529
|
+
const filters: SQL[] = [
|
|
3530
|
+
eq(schema.sessionEvents.workspaceId, workspaceId),
|
|
3531
|
+
eq(schema.sessionEvents.sessionId, sessionId),
|
|
3532
|
+
gt(schema.sessionEvents.sequence, after),
|
|
3533
|
+
];
|
|
3534
|
+
if (before !== undefined && before <= POSTGRES_INT_MAX) {
|
|
3535
|
+
filters.push(lt(schema.sessionEvents.sequence, before));
|
|
3536
|
+
}
|
|
3299
3537
|
const rows = await scopedDb.select().from(schema.sessionEvents)
|
|
3300
|
-
.where(and(
|
|
3301
|
-
.orderBy(asc(schema.sessionEvents.sequence))
|
|
3538
|
+
.where(and(...filters))
|
|
3539
|
+
.orderBy(hasBefore ? desc(schema.sessionEvents.sequence) : asc(schema.sessionEvents.sequence))
|
|
3302
3540
|
.limit(limit);
|
|
3303
|
-
return rows.map(mapEvent);
|
|
3541
|
+
return (hasBefore ? rows.reverse() : rows).map(mapEvent);
|
|
3304
3542
|
});
|
|
3305
3543
|
}
|
|
3306
3544
|
|
|
3545
|
+
function normalizeEventSequence(value: number | undefined, fallback: number): number {
|
|
3546
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
3547
|
+
return fallback;
|
|
3548
|
+
}
|
|
3549
|
+
return Math.floor(value);
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
function normalizeEventLimit(value: number | undefined, fallback: number): number {
|
|
3553
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
3554
|
+
return fallback;
|
|
3555
|
+
}
|
|
3556
|
+
return Math.max(0, Math.floor(value));
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3307
3559
|
export async function getSessionEvent(db: Database, workspaceId: string, eventId: string): Promise<SessionEvent | null> {
|
|
3308
3560
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
3309
3561
|
const [row] = await scopedDb.select().from(schema.sessionEvents).where(and(eq(schema.sessionEvents.workspaceId, workspaceId), eq(schema.sessionEvents.id, eventId))).limit(1);
|
|
@@ -3384,8 +3636,8 @@ export async function getSessionHistoryItems(db: Database, workspaceId: string,
|
|
|
3384
3636
|
|
|
3385
3637
|
/**
|
|
3386
3638
|
* The LIVE conversation-truth read path: only active rows, position-ordered.
|
|
3387
|
-
* After a client-side context compaction this returns [
|
|
3388
|
-
*
|
|
3639
|
+
* After a client-side context compaction this returns [retained user messages,
|
|
3640
|
+
* active summary]; with no compaction yet it equals
|
|
3389
3641
|
* getSessionHistoryItems. The model-facing read path uses this so superseded
|
|
3390
3642
|
* (summarized-away) prefix rows are excluded while the full transcript stays in
|
|
3391
3643
|
* the table as an audit trail.
|
|
@@ -3555,8 +3807,15 @@ export async function applyContextCompaction(db: Database, input: {
|
|
|
3555
3807
|
turnId?: string | null;
|
|
3556
3808
|
/** Active prefix rows with position < boundaryPosition get superseded. */
|
|
3557
3809
|
boundaryPosition: number;
|
|
3558
|
-
/**
|
|
3810
|
+
/** Position for the new summary row. Old boundary mode uses a fractional half-step before the kept tail. */
|
|
3559
3811
|
summaryPosition: number;
|
|
3812
|
+
/**
|
|
3813
|
+
* Optional replacement rows inserted after superseding the old active set.
|
|
3814
|
+
* Used by Codex-parity client compaction to rebuild active history as retained
|
|
3815
|
+
* user messages plus one summary. These rows are synthetic replay rows, so
|
|
3816
|
+
* they intentionally do not inherit the current compaction turn id.
|
|
3817
|
+
*/
|
|
3818
|
+
replacementItems?: Array<{ position: number; item: Record<string, unknown> }>;
|
|
3560
3819
|
summaryItem: Record<string, unknown>;
|
|
3561
3820
|
}): Promise<void> {
|
|
3562
3821
|
await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId }, async (scopedDb) => {
|
|
@@ -3569,6 +3828,20 @@ export async function applyContextCompaction(db: Database, input: {
|
|
|
3569
3828
|
eq(schema.sessionHistoryItems.active, true),
|
|
3570
3829
|
lt(schema.sessionHistoryItems.position, input.boundaryPosition),
|
|
3571
3830
|
));
|
|
3831
|
+
if (input.replacementItems && input.replacementItems.length > 0) {
|
|
3832
|
+
await tx.insert(schema.sessionHistoryItems).values(input.replacementItems.map((entry) => ({
|
|
3833
|
+
accountId: input.accountId,
|
|
3834
|
+
workspaceId: input.workspaceId,
|
|
3835
|
+
sessionId: input.sessionId,
|
|
3836
|
+
turnId: null,
|
|
3837
|
+
position: entry.position,
|
|
3838
|
+
item: sanitizeEventPayload(entry.item),
|
|
3839
|
+
active: true,
|
|
3840
|
+
}))).onConflictDoUpdate({
|
|
3841
|
+
target: [schema.sessionHistoryItems.workspaceId, schema.sessionHistoryItems.sessionId, schema.sessionHistoryItems.position],
|
|
3842
|
+
set: { active: true },
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3572
3845
|
// Insert the summary at its FRACTIONAL position. The supersede step above
|
|
3573
3846
|
// also sets active=false for any rows with position < boundaryPosition —
|
|
3574
3847
|
// which on a RETRY includes the summary itself (it sits below the
|
|
@@ -4215,6 +4488,14 @@ export interface LeaseSnapshot {
|
|
|
4215
4488
|
expiresAt: Date;
|
|
4216
4489
|
}
|
|
4217
4490
|
|
|
4491
|
+
export interface LiveModalSandboxLeaseAttribution {
|
|
4492
|
+
leaseId: string;
|
|
4493
|
+
workspaceId: string;
|
|
4494
|
+
sandboxGroupId: string;
|
|
4495
|
+
instanceId: string | null;
|
|
4496
|
+
liveness: SandboxLeaseLiveness;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4218
4499
|
export interface AcquireLeaseInput {
|
|
4219
4500
|
accountId: string;
|
|
4220
4501
|
workspaceId: string;
|
|
@@ -4514,6 +4795,40 @@ export async function commitWarmingToWarm(db: Database, input: {
|
|
|
4514
4795
|
});
|
|
4515
4796
|
}
|
|
4516
4797
|
|
|
4798
|
+
// §4.2a — leak-proof create attribution. The spawner calls this immediately
|
|
4799
|
+
// after the provider create returns, before display/readiness/setup work. It
|
|
4800
|
+
// intentionally does NOT bump lease_epoch or mark the lease warm; it only makes
|
|
4801
|
+
// the just-created provider id durable while the row is still warming so a
|
|
4802
|
+
// failure/reaper/provider-side sweep can identify and stop it.
|
|
4803
|
+
export async function recordWarmingSandboxCreated(db: Database, input: {
|
|
4804
|
+
accountId: string;
|
|
4805
|
+
workspaceId: string;
|
|
4806
|
+
sandboxGroupId: string;
|
|
4807
|
+
expectedEpoch: number;
|
|
4808
|
+
instanceId: string;
|
|
4809
|
+
resumeBackendId?: string | null;
|
|
4810
|
+
resumeState?: Record<string, unknown> | null;
|
|
4811
|
+
leaseTtlMs: number;
|
|
4812
|
+
}): Promise<{ recorded: boolean; lease: LeaseSnapshot | null }> {
|
|
4813
|
+
return await withRlsContext(db, { accountId: input.accountId, workspaceId: input.workspaceId },
|
|
4814
|
+
async (scopedDb) => {
|
|
4815
|
+
const resumeStateJson = input.resumeState == null ? null : JSON.stringify(input.resumeState);
|
|
4816
|
+
const rows = await scopedDb.execute<LeaseRow>(sql`
|
|
4817
|
+
update sandbox_leases set
|
|
4818
|
+
instance_id = ${input.instanceId},
|
|
4819
|
+
resume_backend_id = ${input.resumeBackendId ?? null},
|
|
4820
|
+
resume_state = ${resumeStateJson}::jsonb,
|
|
4821
|
+
expires_at = now() + (${String(input.leaseTtlMs)} || ' milliseconds')::interval,
|
|
4822
|
+
updated_at = now()
|
|
4823
|
+
where workspace_id = ${input.workspaceId} and sandbox_group_id = ${input.sandboxGroupId}
|
|
4824
|
+
and liveness = 'warming' and lease_epoch = ${input.expectedEpoch}
|
|
4825
|
+
returning *
|
|
4826
|
+
`);
|
|
4827
|
+
if (rows.length === 0) return { recorded: false, lease: null };
|
|
4828
|
+
return { recorded: true, lease: mapLeaseRow(rows[0]!) };
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
|
|
4517
4832
|
// §4.3 — caught spawn failure: warming -> cold (W3). Holders are intentionally
|
|
4518
4833
|
// left intact — the arrival that triggered the spawn still wants a box, so the
|
|
4519
4834
|
// next acquireLease re-CAS cold->warming.
|
|
@@ -4696,15 +5011,34 @@ export async function reapStaleLeaseHolders(db: Database, input: {
|
|
|
4696
5011
|
where L.id = c.id and L.workspace_id = ${input.workspaceId}
|
|
4697
5012
|
`);
|
|
4698
5013
|
|
|
4699
|
-
// (
|
|
4700
|
-
//
|
|
5014
|
+
// (c1) WARMING-death before provider create returned: no instance_id was
|
|
5015
|
+
// ever persisted, so there is no provider box to stop. Reset to cold so a
|
|
5016
|
+
// queued turn can re-acquire and re-spawn.
|
|
4701
5017
|
const warmingReset = await tx.execute<{ id: string }>(sql`
|
|
4702
5018
|
update sandbox_leases set
|
|
4703
5019
|
liveness = 'cold', instance_id = null,
|
|
4704
5020
|
resume_backend_id = null, resume_state = null,
|
|
4705
5021
|
data_plane_url = null, terminal_data_plane_url = null, updated_at = now()
|
|
4706
5022
|
where workspace_id = ${input.workspaceId}
|
|
4707
|
-
and liveness = 'warming' and expires_at < now()
|
|
5023
|
+
and liveness = 'warming' and expires_at < now() and instance_id is null
|
|
5024
|
+
returning id
|
|
5025
|
+
`);
|
|
5026
|
+
|
|
5027
|
+
// (c2) WARMING-death after provider create returned: instance_id is known,
|
|
5028
|
+
// so do NOT drop it. Convert to immediately-drainable so the caller's
|
|
5029
|
+
// provider terminate path stops the box before the lease goes cold.
|
|
5030
|
+
const warmingDrain = await tx.execute<{ id: string }>(sql`
|
|
5031
|
+
update sandbox_leases set
|
|
5032
|
+
liveness = 'draining',
|
|
5033
|
+
refcount = 0,
|
|
5034
|
+
turn_holders = 0,
|
|
5035
|
+
viewer_holders = 0,
|
|
5036
|
+
data_plane_url = null,
|
|
5037
|
+
terminal_data_plane_url = null,
|
|
5038
|
+
expires_at = now() - interval '1 millisecond',
|
|
5039
|
+
updated_at = now()
|
|
5040
|
+
where workspace_id = ${input.workspaceId}
|
|
5041
|
+
and liveness = 'warming' and expires_at < now() and instance_id is not null
|
|
4708
5042
|
returning id
|
|
4709
5043
|
`);
|
|
4710
5044
|
|
|
@@ -4719,7 +5053,7 @@ export async function reapStaleLeaseHolders(db: Database, input: {
|
|
|
4719
5053
|
|
|
4720
5054
|
return {
|
|
4721
5055
|
reapedViewers: reaped.length,
|
|
4722
|
-
warmingReset: warmingReset.length,
|
|
5056
|
+
warmingReset: warmingReset.length + warmingDrain.length,
|
|
4723
5057
|
drained: drainable.map((r) => ({
|
|
4724
5058
|
workspaceId: input.workspaceId,
|
|
4725
5059
|
sandboxGroupId: r.sandbox_group_id,
|
|
@@ -4779,6 +5113,28 @@ export async function listMeterableWarmLeases(db: Database): Promise<MeterableWa
|
|
|
4779
5113
|
}));
|
|
4780
5114
|
}
|
|
4781
5115
|
|
|
5116
|
+
// Cross-workspace live Modal lease read for the provider-side orphan sweep. The
|
|
5117
|
+
// SECURITY DEFINER function is the sanctioned RLS bypass; see migration 0036.
|
|
5118
|
+
export async function listLiveModalSandboxLeaseAttributions(db: Database): Promise<LiveModalSandboxLeaseAttribution[]> {
|
|
5119
|
+
const rows = await rawRows<{
|
|
5120
|
+
lease_id: string;
|
|
5121
|
+
workspace_id: string;
|
|
5122
|
+
sandbox_group_id: string;
|
|
5123
|
+
instance_id: string | null;
|
|
5124
|
+
liveness: SandboxLeaseLiveness;
|
|
5125
|
+
}>(db, sql`
|
|
5126
|
+
select lease_id, workspace_id, sandbox_group_id, instance_id, liveness
|
|
5127
|
+
from opengeni_private.list_live_modal_sandbox_leases()
|
|
5128
|
+
`);
|
|
5129
|
+
return rows.map((r) => ({
|
|
5130
|
+
leaseId: r.lease_id,
|
|
5131
|
+
workspaceId: r.workspace_id,
|
|
5132
|
+
sandboxGroupId: r.sandbox_group_id,
|
|
5133
|
+
instanceId: r.instance_id,
|
|
5134
|
+
liveness: r.liveness,
|
|
5135
|
+
}));
|
|
5136
|
+
}
|
|
5137
|
+
|
|
4782
5138
|
// §4.7 — explicit re-arm seam (D1). acquireLease already re-arms a draining
|
|
4783
5139
|
// lease inline; this is the standalone version for callers that learn a holder
|
|
4784
5140
|
// is wanted during the grace window without going through acquireLease first.
|
|
@@ -7275,7 +7631,11 @@ export async function appendSessionEventsAndUpdateSession(db: Database, workspac
|
|
|
7275
7631
|
}));
|
|
7276
7632
|
}
|
|
7277
7633
|
|
|
7278
|
-
|
|
7634
|
+
type LockedSessionUpdateContext = {
|
|
7635
|
+
updateSessionMcpServerCredentials: (updates: UpdateSessionMcpServerCredentialsInput[]) => Promise<UpdateSessionMcpServerCredentialsResult>;
|
|
7636
|
+
};
|
|
7637
|
+
|
|
7638
|
+
type LockedSessionUpdateResult = {
|
|
7279
7639
|
events: AppendEventInput[];
|
|
7280
7640
|
update?: {
|
|
7281
7641
|
resources?: ResourceRef[];
|
|
@@ -7285,13 +7645,22 @@ export async function appendSessionEventsWithLockedSessionUpdate(db: Database, w
|
|
|
7285
7645
|
status?: SessionStatus;
|
|
7286
7646
|
activeTurnId?: string | null;
|
|
7287
7647
|
};
|
|
7288
|
-
}
|
|
7648
|
+
};
|
|
7649
|
+
|
|
7650
|
+
export async function appendSessionEventsWithLockedSessionUpdate(
|
|
7651
|
+
db: Database,
|
|
7652
|
+
workspaceId: string,
|
|
7653
|
+
sessionId: string,
|
|
7654
|
+
build: (session: Session, context: LockedSessionUpdateContext) => LockedSessionUpdateResult | Promise<LockedSessionUpdateResult>,
|
|
7655
|
+
): Promise<SessionEvent[]> {
|
|
7289
7656
|
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => await scopedDb.transaction(async (tx) => {
|
|
7290
7657
|
const [sessionRow] = await tx.select().from(schema.sessions).where(and(eq(schema.sessions.workspaceId, workspaceId), eq(schema.sessions.id, sessionId))).for("update").limit(1);
|
|
7291
7658
|
if (!sessionRow) {
|
|
7292
7659
|
throw new Error(`Session not found: ${sessionId}`);
|
|
7293
7660
|
}
|
|
7294
|
-
const built = build(mapSession(sessionRow)
|
|
7661
|
+
const built = await build(mapSession(sessionRow), {
|
|
7662
|
+
updateSessionMcpServerCredentials: async (updates) => await updateSessionMcpServerCredentialsInTransaction(tx, { workspaceId, sessionId, updates }),
|
|
7663
|
+
});
|
|
7295
7664
|
if (built.events.length === 0) {
|
|
7296
7665
|
return [];
|
|
7297
7666
|
}
|
|
@@ -7330,7 +7699,7 @@ export function sessionSubject(workspaceId: string, sessionId: string): string {
|
|
|
7330
7699
|
return `workspaces.${workspaceId}.sessions.${sessionId}.events`;
|
|
7331
7700
|
}
|
|
7332
7701
|
|
|
7333
|
-
function mapSession(row: typeof schema.sessions.$inferSelect): Session {
|
|
7702
|
+
function mapSession(row: typeof schema.sessions.$inferSelect, mcpServers: SessionMcpServerMetadata[] = []): Session {
|
|
7334
7703
|
return {
|
|
7335
7704
|
id: row.id,
|
|
7336
7705
|
accountId: row.accountId,
|
|
@@ -7339,6 +7708,7 @@ function mapSession(row: typeof schema.sessions.$inferSelect): Session {
|
|
|
7339
7708
|
initialMessage: row.initialMessage,
|
|
7340
7709
|
title: row.title ?? null,
|
|
7341
7710
|
titleSource: (row.titleSource as "user" | "agent" | null) ?? null,
|
|
7711
|
+
instructions: row.instructions ?? null,
|
|
7342
7712
|
resources: row.resources as ResourceRef[],
|
|
7343
7713
|
tools: row.tools as ToolRef[],
|
|
7344
7714
|
metadata: row.metadata,
|
|
@@ -7353,6 +7723,7 @@ function mapSession(row: typeof schema.sessions.$inferSelect): Session {
|
|
|
7353
7723
|
activeEpoch: Number(row.activeEpoch),
|
|
7354
7724
|
environmentId: row.environmentId,
|
|
7355
7725
|
firstPartyMcpPermissions: (row.firstPartyMcpPermissions as Permission[] | null) ?? null,
|
|
7726
|
+
mcpServers,
|
|
7356
7727
|
parentSessionId: row.parentSessionId ?? null,
|
|
7357
7728
|
createIdempotencyKey: row.createIdempotencyKey ?? null,
|
|
7358
7729
|
temporalWorkflowId: row.temporalWorkflowId,
|
package/src/schema.ts
CHANGED
|
@@ -187,6 +187,13 @@ export const sessions = pgTable("sessions", {
|
|
|
187
187
|
initialMessage: text("initial_message").notNull(),
|
|
188
188
|
title: text("title"),
|
|
189
189
|
titleSource: text("title_source"),
|
|
190
|
+
// Per-session agent persona/system instructions supplied at create (the
|
|
191
|
+
// per-agent-type prompt lever for embedding hosts). NULL ⇒ the session
|
|
192
|
+
// carried none, so the composed agent instructions are byte-identical to a
|
|
193
|
+
// workspace-only persona (no backfill, no behavior change for existing rows).
|
|
194
|
+
// Composed system-level AFTER the workspace agentInstructions; never emitted
|
|
195
|
+
// as a timeline event.
|
|
196
|
+
instructions: text("instructions"),
|
|
190
197
|
resources: jsonb("resources").$type<unknown[]>().notNull().default([]),
|
|
191
198
|
tools: jsonb("tools").$type<unknown[]>().notNull().default([]),
|
|
192
199
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
|
@@ -283,6 +290,28 @@ export const sessions = pgTable("sessions", {
|
|
|
283
290
|
createIdempotency: uniqueIndex("sessions_workspace_create_idempotency_idx").on(table.workspaceId, table.createIdempotencyKey).where(sql`${table.createIdempotencyKey} is not null`),
|
|
284
291
|
}));
|
|
285
292
|
|
|
293
|
+
export const sessionMcpServers = pgTable("session_mcp_servers", {
|
|
294
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
295
|
+
accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
|
|
296
|
+
workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
|
|
297
|
+
sessionId: uuid("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
298
|
+
serverId: text("server_id").notNull(),
|
|
299
|
+
name: text("name"),
|
|
300
|
+
url: text("url").notNull(),
|
|
301
|
+
allowedTools: jsonb("allowed_tools").$type<string[]>(),
|
|
302
|
+
timeoutMs: integer("timeout_ms"),
|
|
303
|
+
cacheToolsList: boolean("cache_tools_list").notNull().default(false),
|
|
304
|
+
// Map of header name -> AES-GCM ciphertext. Values are decrypted only by the
|
|
305
|
+
// worker's run-preparation path and never returned by API helpers.
|
|
306
|
+
headersEncrypted: jsonb("headers_encrypted").$type<Record<string, string>>().notNull().default({}),
|
|
307
|
+
credentialVersion: integer("credential_version").notNull().default(1),
|
|
308
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
309
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
310
|
+
}, (table) => ({
|
|
311
|
+
sessionServer: uniqueIndex("session_mcp_servers_session_server_idx").on(table.workspaceId, table.sessionId, table.serverId),
|
|
312
|
+
session: index("session_mcp_servers_session_idx").on(table.workspaceId, table.sessionId),
|
|
313
|
+
}));
|
|
314
|
+
|
|
286
315
|
export const files = pgTable("files", {
|
|
287
316
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
288
317
|
accountId: uuid("account_id").notNull().references(() => managedAccounts.id, { onDelete: "cascade" }),
|