@opengeni/api-router 0.5.7 → 0.9.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/app.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-QOYQBYHM.js} +4302 -926
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/app.ts +83 -18
- package/src/codex-redemption-security.ts +96 -0
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +828 -146
- package/src/mcp/session-view.ts +721 -201
- package/src/mcp/toolspace.ts +482 -132
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +883 -28
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/files.ts +153 -0
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +789 -81
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +66 -13
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +8 -2
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CreateScheduledTaskRequest,
|
|
3
|
+
defaultRepositoryMountPath,
|
|
4
|
+
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
5
|
+
SessionEventLatestClass,
|
|
6
|
+
SessionEventPayloadMode,
|
|
7
|
+
SessionEventReadDirection,
|
|
8
|
+
SessionEventReadMode,
|
|
9
|
+
SessionEventResultMode,
|
|
10
|
+
SessionEventSemanticClass,
|
|
11
|
+
SessionEventType,
|
|
12
|
+
compactSessionEventResult,
|
|
13
|
+
sessionEventLatestClassToSemanticClass,
|
|
3
14
|
SessionMcpCredentialUpdateInput,
|
|
4
15
|
VariableSetVariableName,
|
|
5
16
|
type AccessGrant,
|
|
6
17
|
type GitHubRepository,
|
|
7
18
|
type Permission,
|
|
8
19
|
type ResourceRef,
|
|
20
|
+
type SessionAuthorizationOperation,
|
|
21
|
+
type Session,
|
|
9
22
|
UpdateScheduledTaskRequest,
|
|
10
23
|
} from "@opengeni/contracts";
|
|
11
24
|
import {
|
|
@@ -21,12 +34,18 @@ import {
|
|
|
21
34
|
getSessionTurn,
|
|
22
35
|
getVariableSet,
|
|
23
36
|
getVariableSetByName,
|
|
24
|
-
|
|
37
|
+
areGitHubRepositoriesAllowedForWorkspace,
|
|
25
38
|
listScheduledTaskRuns,
|
|
26
39
|
listScheduledTasks,
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
listSessionEventPage,
|
|
41
|
+
listSessionDiscoverySummaries,
|
|
42
|
+
projectEffectiveControlForRelatedAccess,
|
|
43
|
+
projectSessionForRelatedAccess,
|
|
44
|
+
type SessionDiscoveryCursor,
|
|
45
|
+
type SessionDiscoveryOrderBy,
|
|
29
46
|
listRigs,
|
|
47
|
+
listRigChangeMonitoringSummaries,
|
|
48
|
+
listRigVersionMonitoringSummaries,
|
|
30
49
|
listSocialConnections,
|
|
31
50
|
listSocialPosts,
|
|
32
51
|
listVariableSets,
|
|
@@ -38,6 +57,7 @@ import {
|
|
|
38
57
|
requireSession,
|
|
39
58
|
saveWorkspaceMemory,
|
|
40
59
|
searchWorkspaceMemories,
|
|
60
|
+
serializeEffectiveSessionControl,
|
|
41
61
|
setSessionGoalStatus,
|
|
42
62
|
setVariableSetVariable,
|
|
43
63
|
updateScheduledTask,
|
|
@@ -49,20 +69,21 @@ import {
|
|
|
49
69
|
import { appendAndPublishEvents } from "@opengeni/events";
|
|
50
70
|
import {
|
|
51
71
|
createGitHubAppInstallationToken,
|
|
52
|
-
createSignedState,
|
|
53
72
|
GitHubAppConfigurationError,
|
|
54
73
|
githubAppMissingSettings,
|
|
55
|
-
listGitHubAppRepositories,
|
|
56
|
-
stateMaxAgeSeconds,
|
|
57
74
|
} from "@opengeni/github";
|
|
58
75
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
59
76
|
import * as z4 from "zod/v4";
|
|
60
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
hasPermission,
|
|
79
|
+
requireSessionAuthorization,
|
|
80
|
+
requireSessionAuthorizationListScope,
|
|
81
|
+
type ResolvedSessionAuthorization,
|
|
82
|
+
} from "@opengeni/core";
|
|
61
83
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
62
84
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
85
|
+
import { listWorkspaceGitHubRepositories } from "../github-access";
|
|
63
86
|
import {
|
|
64
|
-
listRigChangesForApi,
|
|
65
|
-
listRigVersionsForApi,
|
|
66
87
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
67
88
|
proposeRigChangeForApi,
|
|
68
89
|
requireRigChangeForApi,
|
|
@@ -91,6 +112,9 @@ import {
|
|
|
91
112
|
sendAgentSessionMessage,
|
|
92
113
|
steerAgentSession,
|
|
93
114
|
updateSessionTitle,
|
|
115
|
+
sessionWithEffectiveToolPolicy,
|
|
116
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
117
|
+
workspaceSessionToolPolicyServerIds,
|
|
94
118
|
type AgentSessionCommandContext,
|
|
95
119
|
} from "@opengeni/core";
|
|
96
120
|
import {
|
|
@@ -103,13 +127,19 @@ import {
|
|
|
103
127
|
type FleetServices,
|
|
104
128
|
type RunOnOp,
|
|
105
129
|
} from "@opengeni/core";
|
|
106
|
-
import {
|
|
130
|
+
import {
|
|
131
|
+
boundSessionEventCompactResult,
|
|
132
|
+
boundSessionEventMcpPage,
|
|
133
|
+
boundSessionDetailMcp,
|
|
134
|
+
boundRigDetailMcp,
|
|
135
|
+
SESSION_EVENT_MCP_MAX_BYTES,
|
|
136
|
+
} from "./session-view";
|
|
107
137
|
import type { ToolspaceMcpSurface } from "./toolspace";
|
|
108
138
|
|
|
109
139
|
export type McpServerOptions = {
|
|
110
|
-
// Origin of the HTTP request that reached the MCP route
|
|
111
|
-
//
|
|
112
|
-
//
|
|
140
|
+
// Origin of the HTTP request that reached the MCP route. Retained in the
|
|
141
|
+
// options ABI for browser-oriented tools; github_connect_link does not use
|
|
142
|
+
// it or mint state while new installation binding is disabled.
|
|
113
143
|
requestOrigin?: string | null;
|
|
114
144
|
toolspace?: ToolspaceMcpSurface | null;
|
|
115
145
|
workspaceMemoryEnabled?: boolean | undefined;
|
|
@@ -148,8 +178,13 @@ export function buildOpenGeniMcpServer(
|
|
|
148
178
|
inputSchema: { title: z4.string().min(1).max(200) },
|
|
149
179
|
},
|
|
150
180
|
async ({ title }) => {
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.title.write");
|
|
182
|
+
const result = await updateSessionTitle(deps, grant, sessionId, title, "agent");
|
|
183
|
+
return json({
|
|
184
|
+
ok: true,
|
|
185
|
+
updated: result.updated,
|
|
186
|
+
title: result.title ?? title,
|
|
187
|
+
});
|
|
153
188
|
},
|
|
154
189
|
);
|
|
155
190
|
}
|
|
@@ -178,19 +213,20 @@ export function buildOpenGeniMcpServer(
|
|
|
178
213
|
registerRigTools(server, deps, grant, can, sessionId, json);
|
|
179
214
|
}
|
|
180
215
|
|
|
181
|
-
// Orchestration, variableSet, and GitHub
|
|
216
|
+
// Orchestration, variableSet, and GitHub status/token tools are permission-gated
|
|
182
217
|
// at registration: a grant without the permission does not see the tool.
|
|
183
218
|
// Sandboxed workers reach this server with the first-party delegated
|
|
184
219
|
// permission set (firstPartyMcpPermissions in @opengeni/runtime), which is
|
|
185
220
|
// POWERFUL BY DEFAULT — it carries sessions:*, variable sets:*, and github:use,
|
|
186
|
-
// so agents can spawn/read sessions, manage variable set variables,
|
|
187
|
-
//
|
|
221
|
+
// so agents can spawn/read sessions, manage variable set variables, inspect
|
|
222
|
+
// GitHub connection availability, and refresh scoped tokens for already-bound
|
|
223
|
+
// repositories out of the box. A user DEMOTES a specific
|
|
188
224
|
// session by setting a narrower session.firstPartyMcpPermissions (capped to
|
|
189
225
|
// the creator's own grant); operators still cap what any session can be given.
|
|
190
226
|
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, toolspaceMode, json);
|
|
191
227
|
registerVariableSetTools(server, deps, grant, can, json);
|
|
192
228
|
if (can("github:use")) {
|
|
193
|
-
registerGitHubConnectTool(server, deps,
|
|
229
|
+
registerGitHubConnectTool(server, deps, json);
|
|
194
230
|
// TOKEN-BROKER (B1): the agent-refreshable git token. Session-scoped (keys off the
|
|
195
231
|
// worker-signed sessionId claim so it mints for THIS session's repos), gated on
|
|
196
232
|
// the same github:use capability as github_connect_link.
|
|
@@ -214,7 +250,9 @@ export function buildOpenGeniMcpServer(
|
|
|
214
250
|
if (file.status !== "ready") {
|
|
215
251
|
throw new Error(`file is ${file.status}`);
|
|
216
252
|
}
|
|
217
|
-
const signed = await deps.objectStorage.createGetUrl({
|
|
253
|
+
const signed = await deps.objectStorage.createGetUrl({
|
|
254
|
+
key: file.objectKey,
|
|
255
|
+
});
|
|
218
256
|
return json({
|
|
219
257
|
file: {
|
|
220
258
|
id: file.id,
|
|
@@ -246,11 +284,7 @@ export function buildOpenGeniMcpServer(
|
|
|
246
284
|
},
|
|
247
285
|
async ({ limit }) => {
|
|
248
286
|
try {
|
|
249
|
-
const
|
|
250
|
-
deps.db,
|
|
251
|
-
grant.workspaceId,
|
|
252
|
-
);
|
|
253
|
-
const repositories = await listGitHubAppRepositories(deps.settings, { installationIds });
|
|
287
|
+
const repositories = await listWorkspaceGitHubRepositories(deps, grant.workspaceId);
|
|
254
288
|
const visible = typeof limit === "number" ? repositories.slice(0, limit) : repositories;
|
|
255
289
|
return json({
|
|
256
290
|
repositories: visible.map((repository) =>
|
|
@@ -380,7 +414,9 @@ export function buildOpenGeniMcpServer(
|
|
|
380
414
|
inputSchema: { limit: z4.number().int().positive().optional() },
|
|
381
415
|
},
|
|
382
416
|
async ({ limit }) =>
|
|
383
|
-
json({
|
|
417
|
+
json({
|
|
418
|
+
tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100),
|
|
419
|
+
}),
|
|
384
420
|
);
|
|
385
421
|
|
|
386
422
|
server.registerTool(
|
|
@@ -429,7 +465,11 @@ export function buildOpenGeniMcpServer(
|
|
|
429
465
|
payload,
|
|
430
466
|
toolsProvided: scheduledTaskToolsProvided(args),
|
|
431
467
|
});
|
|
432
|
-
await syncCreatedScheduledTask({
|
|
468
|
+
await syncCreatedScheduledTask({
|
|
469
|
+
db: deps.db,
|
|
470
|
+
workflowClient: deps.workflowClient,
|
|
471
|
+
task,
|
|
472
|
+
});
|
|
433
473
|
return json(task);
|
|
434
474
|
},
|
|
435
475
|
);
|
|
@@ -526,7 +566,10 @@ export function buildOpenGeniMcpServer(
|
|
|
526
566
|
{
|
|
527
567
|
description:
|
|
528
568
|
"Trigger a scheduled task immediately. Pass a stable triggerId to make a retried trigger idempotent (one charge, one run).",
|
|
529
|
-
inputSchema: {
|
|
569
|
+
inputSchema: {
|
|
570
|
+
id: z4.string().uuid(),
|
|
571
|
+
triggerId: z4.string().min(1).max(128).optional(),
|
|
572
|
+
},
|
|
530
573
|
},
|
|
531
574
|
async ({ id, triggerId }) => {
|
|
532
575
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
@@ -584,7 +627,10 @@ export function buildOpenGeniMcpServer(
|
|
|
584
627
|
"scheduled_task_runs_list",
|
|
585
628
|
{
|
|
586
629
|
description: "List runs for a scheduled task.",
|
|
587
|
-
inputSchema: {
|
|
630
|
+
inputSchema: {
|
|
631
|
+
taskId: z4.string().uuid(),
|
|
632
|
+
limit: z4.number().int().positive().optional(),
|
|
633
|
+
},
|
|
588
634
|
},
|
|
589
635
|
async ({ taskId, limit }) =>
|
|
590
636
|
json({
|
|
@@ -686,6 +732,7 @@ function registerGoalTools(
|
|
|
686
732
|
},
|
|
687
733
|
},
|
|
688
734
|
async ({ text, successCriteria, maxAutoContinuations }) => {
|
|
735
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
689
736
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
690
737
|
const callerTurnId =
|
|
691
738
|
typeof grant.metadata?.["turnId"] === "string"
|
|
@@ -730,6 +777,7 @@ function registerGoalTools(
|
|
|
730
777
|
},
|
|
731
778
|
},
|
|
732
779
|
async ({ text, successCriteria, progressNote }) => {
|
|
780
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
733
781
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
734
782
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
735
783
|
if (!existing) {
|
|
@@ -767,6 +815,7 @@ function registerGoalTools(
|
|
|
767
815
|
inputSchema: { evidence: z4.string().min(1) },
|
|
768
816
|
},
|
|
769
817
|
async ({ evidence }) => {
|
|
818
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
770
819
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
771
820
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
772
821
|
if (!existing) {
|
|
@@ -796,6 +845,7 @@ function registerGoalTools(
|
|
|
796
845
|
inputSchema: { rationale: z4.string().min(1) },
|
|
797
846
|
},
|
|
798
847
|
async ({ rationale }) => {
|
|
848
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
799
849
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
800
850
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
801
851
|
if (!existing) {
|
|
@@ -826,7 +876,22 @@ function registerGoalTools(
|
|
|
826
876
|
);
|
|
827
877
|
}
|
|
828
878
|
|
|
829
|
-
type JsonResult = (value: unknown) => {
|
|
879
|
+
type JsonResult = (value: unknown) => {
|
|
880
|
+
content: Array<{ type: "text"; text: string }>;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
async function authorizeFirstPartySession(
|
|
884
|
+
deps: ApiRouteDeps,
|
|
885
|
+
grant: AccessGrant,
|
|
886
|
+
sessionId: string,
|
|
887
|
+
operation: SessionAuthorizationOperation,
|
|
888
|
+
): Promise<ResolvedSessionAuthorization | null> {
|
|
889
|
+
return await requireSessionAuthorization(deps, grant, {
|
|
890
|
+
sessionId,
|
|
891
|
+
operation,
|
|
892
|
+
surface: "first_party_mcp",
|
|
893
|
+
});
|
|
894
|
+
}
|
|
830
895
|
|
|
831
896
|
const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
|
|
832
897
|
|
|
@@ -969,7 +1034,11 @@ function registerFleetTools(
|
|
|
969
1034
|
sessionId: string,
|
|
970
1035
|
json: JsonResult,
|
|
971
1036
|
): void {
|
|
972
|
-
const services: FleetServices = {
|
|
1037
|
+
const services: FleetServices = {
|
|
1038
|
+
db: deps.db,
|
|
1039
|
+
settings: deps.settings,
|
|
1040
|
+
bus: deps.bus,
|
|
1041
|
+
};
|
|
973
1042
|
|
|
974
1043
|
// Resolve the session's group sandbox (the default/home fleet member) at
|
|
975
1044
|
// call-time via the shared helper (same context the user-authenticated swap
|
|
@@ -1026,7 +1095,11 @@ function registerFleetTools(
|
|
|
1026
1095
|
workdir: z4.string().optional(),
|
|
1027
1096
|
}),
|
|
1028
1097
|
z4.object({ kind: z4.literal("read"), path: z4.string().min(1) }),
|
|
1029
|
-
z4.object({
|
|
1098
|
+
z4.object({
|
|
1099
|
+
kind: z4.literal("write"),
|
|
1100
|
+
path: z4.string().min(1),
|
|
1101
|
+
content: z4.string(),
|
|
1102
|
+
}),
|
|
1030
1103
|
]),
|
|
1031
1104
|
},
|
|
1032
1105
|
},
|
|
@@ -1046,7 +1119,10 @@ function registerFleetTools(
|
|
|
1046
1119
|
},
|
|
1047
1120
|
async ({ kind, name }) =>
|
|
1048
1121
|
json(
|
|
1049
|
-
await provisionSandbox(services, await fleetContext(), {
|
|
1122
|
+
await provisionSandbox(services, await fleetContext(), {
|
|
1123
|
+
kind,
|
|
1124
|
+
...(name ? { name } : {}),
|
|
1125
|
+
}),
|
|
1050
1126
|
),
|
|
1051
1127
|
);
|
|
1052
1128
|
}
|
|
@@ -1100,24 +1176,31 @@ function registerRigTools(
|
|
|
1100
1176
|
server.registerTool(
|
|
1101
1177
|
"rig_get",
|
|
1102
1178
|
{
|
|
1103
|
-
description:
|
|
1179
|
+
description:
|
|
1180
|
+
"Get one rig's bounded active definition plus compact historical version/change summaries. Historical setup scripts, checks, payloads, and verification logs are represented by counts/byte facts rather than copied into model context; use the access-controlled REST detail endpoints for exact retained definitions.",
|
|
1104
1181
|
inputSchema: {
|
|
1105
1182
|
rigId: z4.string().uuid(),
|
|
1183
|
+
versionLimit: z4.number().int().positive().optional(),
|
|
1106
1184
|
changeLimit: z4.number().int().positive().optional(),
|
|
1107
1185
|
},
|
|
1108
1186
|
},
|
|
1109
|
-
async ({ rigId, changeLimit }) => {
|
|
1187
|
+
async ({ rigId, versionLimit, changeLimit }) => {
|
|
1110
1188
|
const rig = await requireRigForApi(deps.db, grant.workspaceId, rigId);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
changes: await listRigChangesForApi(
|
|
1115
|
-
{ db: deps.db },
|
|
1189
|
+
const [versions, changes] = await Promise.all([
|
|
1190
|
+
listRigVersionMonitoringSummaries(
|
|
1191
|
+
deps.db,
|
|
1116
1192
|
grant.workspaceId,
|
|
1117
1193
|
rig.id,
|
|
1118
|
-
|
|
1194
|
+
boundedRigHistoryLimit(versionLimit),
|
|
1119
1195
|
),
|
|
1120
|
-
|
|
1196
|
+
listRigChangeMonitoringSummaries(
|
|
1197
|
+
deps.db,
|
|
1198
|
+
grant.workspaceId,
|
|
1199
|
+
rig.id,
|
|
1200
|
+
boundedRigHistoryLimit(changeLimit),
|
|
1201
|
+
),
|
|
1202
|
+
]);
|
|
1203
|
+
return json(boundRigDetailMcp(rig, versions, changes));
|
|
1121
1204
|
},
|
|
1122
1205
|
);
|
|
1123
1206
|
|
|
@@ -1237,6 +1320,7 @@ function exactAgentCommandContext(
|
|
|
1237
1320
|
return {
|
|
1238
1321
|
accountId: grant.accountId,
|
|
1239
1322
|
workspaceId: grant.workspaceId,
|
|
1323
|
+
subjectId: grant.subjectId,
|
|
1240
1324
|
callerSessionId,
|
|
1241
1325
|
callerTurnId: turnId,
|
|
1242
1326
|
callerAttemptId: attemptId,
|
|
@@ -1257,11 +1341,47 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1257
1341
|
server.registerTool(
|
|
1258
1342
|
"sessions_list",
|
|
1259
1343
|
{
|
|
1260
|
-
description:
|
|
1261
|
-
inputSchema: {
|
|
1344
|
+
description: `List compact high-level session status in this workspace. Defaults to creation order; use orderBy=updatedAt with decimal activity-revision updatedAfter/updatedThrough tokens for gap-free indexed incremental monitoring independent of application clocks. Cursors are opaque revision-fenced keysets. includeLastMessage is opt-in; its rendered previews share a deterministic ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 aggregate budget, and omitted previews include a bounded session_events drill-down input (exact message type, direction=before, limit=1, monitoring summary). Use session_get for exact known targets and detailed resources/tools/settings. The list never returns full session objects or history.`,
|
|
1345
|
+
inputSchema: {
|
|
1346
|
+
limit: z4.number().int().positive().max(100).optional(),
|
|
1347
|
+
cursor: z4.string().max(512).optional(),
|
|
1348
|
+
includeLastMessage: z4.boolean().optional(),
|
|
1349
|
+
orderBy: z4.enum(["createdAt", "updatedAt"]).optional(),
|
|
1350
|
+
updatedAfter: z4.string().max(64).optional(),
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
async ({ limit, cursor, includeLastMessage, orderBy: requestedOrderBy, updatedAfter }) => {
|
|
1354
|
+
const authorizationScope = await requireSessionAuthorizationListScope(
|
|
1355
|
+
deps,
|
|
1356
|
+
grant,
|
|
1357
|
+
"first_party_mcp",
|
|
1358
|
+
);
|
|
1359
|
+
const decodedCursor = cursor ? decodeSessionDiscoveryCursor(cursor) : undefined;
|
|
1360
|
+
const orderBy: SessionDiscoveryOrderBy =
|
|
1361
|
+
requestedOrderBy ?? decodedCursor?.orderBy ?? "createdAt";
|
|
1362
|
+
if (decodedCursor && decodedCursor.orderBy !== orderBy) {
|
|
1363
|
+
throw new Error("sessions_list cursor order does not match orderBy");
|
|
1364
|
+
}
|
|
1365
|
+
const normalizedUpdatedAfter =
|
|
1366
|
+
updatedAfter !== undefined
|
|
1367
|
+
? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter")
|
|
1368
|
+
: (decodedCursor?.updatedAfter ?? undefined);
|
|
1369
|
+
if (normalizedUpdatedAfter !== undefined && orderBy !== "updatedAt") {
|
|
1370
|
+
throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
|
|
1371
|
+
}
|
|
1372
|
+
if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
|
|
1373
|
+
throw new Error("sessions_list cursor does not match updatedAfter");
|
|
1374
|
+
}
|
|
1375
|
+
const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
|
|
1376
|
+
limit: boundedSessionDiscoveryLimit(limit),
|
|
1377
|
+
...(decodedCursor ? { cursor: decodedCursor } : {}),
|
|
1378
|
+
includeLastMessage: includeLastMessage === true,
|
|
1379
|
+
orderBy,
|
|
1380
|
+
...(normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {}),
|
|
1381
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
1382
|
+
});
|
|
1383
|
+
return json(capSessionDiscoveryPage(page, includeLastMessage === true));
|
|
1262
1384
|
},
|
|
1263
|
-
async ({ limit }) =>
|
|
1264
|
-
json({ sessions: await listSessions(deps.db, grant.workspaceId, boundedMcpLimit(limit)) }),
|
|
1265
1385
|
);
|
|
1266
1386
|
|
|
1267
1387
|
server.registerTool(
|
|
@@ -1272,15 +1392,32 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1272
1392
|
inputSchema: { sessionId: z4.string().uuid() },
|
|
1273
1393
|
},
|
|
1274
1394
|
async ({ sessionId }) => {
|
|
1395
|
+
const authorization = await authorizeFirstPartySession(
|
|
1396
|
+
deps,
|
|
1397
|
+
grant,
|
|
1398
|
+
sessionId,
|
|
1399
|
+
"session.read",
|
|
1400
|
+
);
|
|
1275
1401
|
const session = await getSession(deps.db, grant.workspaceId, sessionId);
|
|
1276
1402
|
if (!session) {
|
|
1277
1403
|
throw new Error("session not found");
|
|
1278
1404
|
}
|
|
1279
1405
|
const queue = await getSessionQueueSnapshot(deps.db, grant.workspaceId, sessionId);
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1406
|
+
const projected = projectSessionForRelatedAccess(
|
|
1407
|
+
{
|
|
1408
|
+
...session,
|
|
1409
|
+
effectiveControl: queue?.effectiveControl ?? session.effectiveControl,
|
|
1410
|
+
},
|
|
1411
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1412
|
+
);
|
|
1413
|
+
return json(
|
|
1414
|
+
boundSessionDetailMcp(
|
|
1415
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, {
|
|
1416
|
+
...projected,
|
|
1417
|
+
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
|
|
1418
|
+
}),
|
|
1419
|
+
),
|
|
1420
|
+
);
|
|
1284
1421
|
},
|
|
1285
1422
|
);
|
|
1286
1423
|
|
|
@@ -1288,28 +1425,108 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1288
1425
|
"session_events",
|
|
1289
1426
|
{
|
|
1290
1427
|
description:
|
|
1291
|
-
"Read a
|
|
1428
|
+
"Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Use `latest` as an exclusive lookup for the authoritative newest durable sequence in exactly one semantic class; `receipt` is the concise alias for `tool_receipt`, and latest cannot be combined with type or class filters. Add `resultMode=compact` to latest for one bounded result-bearing completion/checkpoint/receipt without another inference. Use nextBefore to page older or explicit after/nextAfter to page forward. Type/class filters run in the RLS-scoped database query. payloadMode none|summary|full controls retained audit payload projection, but every model result is independently byte-capped with explicit truncation and exact covered sequence bounds. Exact retained forensic payloads require the access-controlled REST/SDK events API with mode=forensic&payloadMode=full; generic source bytes never retained by the audit boundary remain unavailable.",
|
|
1292
1429
|
inputSchema: {
|
|
1293
1430
|
sessionId: z4.string().uuid(),
|
|
1294
1431
|
after: z4.number().int().nonnegative().optional(),
|
|
1432
|
+
before: z4.number().int().positive().optional(),
|
|
1295
1433
|
limit: z4.number().int().positive().optional(),
|
|
1434
|
+
direction: z4.enum(SessionEventReadDirection.options).optional(),
|
|
1435
|
+
mode: z4.enum(SessionEventReadMode.options).optional(),
|
|
1436
|
+
payloadMode: z4.enum(SessionEventPayloadMode.options).optional(),
|
|
1437
|
+
resultMode: z4.enum(SessionEventResultMode.options).optional(),
|
|
1438
|
+
includeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1439
|
+
excludeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1440
|
+
includeClasses: z4
|
|
1441
|
+
.array(z4.enum(SessionEventSemanticClass.options))
|
|
1442
|
+
.max(SessionEventSemanticClass.options.length)
|
|
1443
|
+
.optional(),
|
|
1444
|
+
excludeClasses: z4
|
|
1445
|
+
.array(z4.enum(SessionEventSemanticClass.options))
|
|
1446
|
+
.max(SessionEventSemanticClass.options.length)
|
|
1447
|
+
.optional(),
|
|
1448
|
+
latest: z4.enum(SessionEventLatestClass.options).optional(),
|
|
1296
1449
|
},
|
|
1297
1450
|
},
|
|
1298
|
-
async ({
|
|
1451
|
+
async ({
|
|
1452
|
+
sessionId,
|
|
1453
|
+
after,
|
|
1454
|
+
before,
|
|
1455
|
+
limit,
|
|
1456
|
+
direction: requestedDirection,
|
|
1457
|
+
mode: requestedMode,
|
|
1458
|
+
payloadMode: requestedPayloadMode,
|
|
1459
|
+
resultMode: requestedResultMode,
|
|
1460
|
+
includeTypes,
|
|
1461
|
+
excludeTypes,
|
|
1462
|
+
includeClasses,
|
|
1463
|
+
excludeClasses,
|
|
1464
|
+
latest,
|
|
1465
|
+
}) => {
|
|
1466
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.events.read");
|
|
1467
|
+
const latestClass =
|
|
1468
|
+
latest === undefined ? undefined : sessionEventLatestClassToSemanticClass(latest);
|
|
1469
|
+
if (requestedResultMode === "compact" && latestClass === undefined) {
|
|
1470
|
+
throw new Error("resultMode=compact requires latest");
|
|
1471
|
+
}
|
|
1299
1472
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1473
|
+
if (
|
|
1474
|
+
latest &&
|
|
1475
|
+
[includeTypes, excludeTypes, includeClasses, excludeClasses].some(
|
|
1476
|
+
(filter) => filter !== undefined,
|
|
1477
|
+
)
|
|
1478
|
+
) {
|
|
1479
|
+
throw new Error("latest cannot be combined with event filters");
|
|
1480
|
+
}
|
|
1481
|
+
const mode = requestedMode ?? (after !== undefined ? "forensic" : "monitoring");
|
|
1482
|
+
const direction = latestClass
|
|
1483
|
+
? "before"
|
|
1484
|
+
: (requestedDirection ??
|
|
1485
|
+
(before !== undefined ? "before" : after !== undefined ? "after" : "before"));
|
|
1486
|
+
const payloadMode =
|
|
1487
|
+
requestedResultMode === "compact"
|
|
1488
|
+
? "full"
|
|
1489
|
+
: (requestedPayloadMode ?? (mode === "monitoring" ? "summary" : "full"));
|
|
1490
|
+
const dbPage = await listSessionEventPage(deps.db, grant.workspaceId, sessionId, {
|
|
1491
|
+
after: after ?? 0,
|
|
1492
|
+
...(before !== undefined ? { before } : {}),
|
|
1493
|
+
direction,
|
|
1494
|
+
limit: latestClass ? 1 : boundedSessionEventMcpLimit(limit),
|
|
1495
|
+
payloadMode,
|
|
1496
|
+
includeTypes: includeTypes ?? [],
|
|
1497
|
+
excludeTypes: excludeTypes ?? [],
|
|
1498
|
+
includeClasses: latestClass ? [latestClass] : (includeClasses ?? []),
|
|
1499
|
+
excludeClasses: excludeClasses ?? [],
|
|
1500
|
+
...(mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {}),
|
|
1501
|
+
...(latestClass ? { authoritativeLatest: true } : {}),
|
|
1502
|
+
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
1312
1503
|
});
|
|
1504
|
+
if (requestedResultMode === "compact") {
|
|
1505
|
+
const event = dbPage.events[0];
|
|
1506
|
+
return json(
|
|
1507
|
+
event
|
|
1508
|
+
? boundSessionEventCompactResult(
|
|
1509
|
+
compactSessionEventResult(
|
|
1510
|
+
event,
|
|
1511
|
+
latestClass!,
|
|
1512
|
+
dbPage.coveredSequence ?? { first: event.sequence, last: event.sequence },
|
|
1513
|
+
),
|
|
1514
|
+
)
|
|
1515
|
+
: null,
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
return json(
|
|
1519
|
+
boundSessionEventMcpPage({
|
|
1520
|
+
events: dbPage.events,
|
|
1521
|
+
mode,
|
|
1522
|
+
payloadMode,
|
|
1523
|
+
direction,
|
|
1524
|
+
sourceHasMore: dbPage.hasMore,
|
|
1525
|
+
sourceTruncatedBy: dbPage.truncatedBy,
|
|
1526
|
+
after: after ?? 0,
|
|
1527
|
+
before: before ?? null,
|
|
1528
|
+
}),
|
|
1529
|
+
);
|
|
1313
1530
|
},
|
|
1314
1531
|
);
|
|
1315
1532
|
}
|
|
@@ -1360,7 +1577,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1360
1577
|
idempotencyKey: z4.string().min(1).max(200).optional(),
|
|
1361
1578
|
// First-party MCP token permissions for the spawned session; every
|
|
1362
1579
|
// permission must be held by this grant (validated in the domain).
|
|
1363
|
-
|
|
1580
|
+
// A goal requires goals:manage in the resulting set; it is never
|
|
1581
|
+
// silently added beyond the inherited or explicit authority.
|
|
1582
|
+
firstPartyMcpPermissions: z4
|
|
1583
|
+
.array(z4.string())
|
|
1584
|
+
.optional()
|
|
1585
|
+
.describe(
|
|
1586
|
+
"Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly.",
|
|
1587
|
+
),
|
|
1364
1588
|
// Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
|
|
1365
1589
|
// creator's box — one filesystem/repo/desktop, N independent conversations;
|
|
1366
1590
|
// this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
|
|
@@ -1394,7 +1618,13 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1394
1618
|
// arbitrary session's wake channel without sessions:control on it.
|
|
1395
1619
|
},
|
|
1396
1620
|
},
|
|
1397
|
-
async (args) =>
|
|
1621
|
+
async (args) => {
|
|
1622
|
+
if (callerSessionId !== null) {
|
|
1623
|
+
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
1624
|
+
}
|
|
1625
|
+
const created = await createSessionForRequest(deps, grant, grant.workspaceId, args);
|
|
1626
|
+
return json(await withMcpEffectivePolicy(deps, grant.workspaceId, created));
|
|
1627
|
+
},
|
|
1398
1628
|
);
|
|
1399
1629
|
}
|
|
1400
1630
|
|
|
@@ -1414,6 +1644,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1414
1644
|
},
|
|
1415
1645
|
},
|
|
1416
1646
|
async ({ sessionId: targetSessionId, text, idempotencyKey, mcpCredentialUpdates }) => {
|
|
1647
|
+
await authorizeFirstPartySession(deps, grant, targetSessionId, "session.append");
|
|
1417
1648
|
if (callerSessionId !== null) {
|
|
1418
1649
|
if ((mcpCredentialUpdates?.length ?? 0) > 0) {
|
|
1419
1650
|
throw new Error("internal session updates cannot change MCP credentials");
|
|
@@ -1464,6 +1695,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1464
1695
|
},
|
|
1465
1696
|
},
|
|
1466
1697
|
async ({ sessionId, idempotencyKey, reason }) => {
|
|
1698
|
+
const authorization = await authorizeFirstPartySession(
|
|
1699
|
+
deps,
|
|
1700
|
+
grant,
|
|
1701
|
+
sessionId,
|
|
1702
|
+
"session.control",
|
|
1703
|
+
);
|
|
1467
1704
|
if (callerSessionId !== null) {
|
|
1468
1705
|
const controlled = await controlAgentSessionWorkstream(
|
|
1469
1706
|
deps,
|
|
@@ -1477,27 +1714,37 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1477
1714
|
);
|
|
1478
1715
|
return json({
|
|
1479
1716
|
receiptId: controlled.receipt.id,
|
|
1480
|
-
effectiveControl:
|
|
1717
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1718
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
1719
|
+
sessionId,
|
|
1720
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1721
|
+
),
|
|
1481
1722
|
interruptionCount: controlled.interruptionCount,
|
|
1482
1723
|
replay: controlled.replay,
|
|
1483
1724
|
});
|
|
1484
1725
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
},
|
|
1499
|
-
),
|
|
1726
|
+
const controlled = await controlHumanSessionWorkstream(
|
|
1727
|
+
deps,
|
|
1728
|
+
{
|
|
1729
|
+
accountId: grant.accountId,
|
|
1730
|
+
workspaceId: grant.workspaceId,
|
|
1731
|
+
sessionId,
|
|
1732
|
+
subjectId: grant.subjectId,
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
action: "pause",
|
|
1736
|
+
clientEventId: idempotencyKey,
|
|
1737
|
+
...(reason ? { reason } : {}),
|
|
1738
|
+
},
|
|
1500
1739
|
);
|
|
1740
|
+
return json({
|
|
1741
|
+
...controlled,
|
|
1742
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1743
|
+
controlled.effectiveControl,
|
|
1744
|
+
sessionId,
|
|
1745
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1746
|
+
),
|
|
1747
|
+
});
|
|
1501
1748
|
},
|
|
1502
1749
|
);
|
|
1503
1750
|
|
|
@@ -1513,6 +1760,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1513
1760
|
},
|
|
1514
1761
|
},
|
|
1515
1762
|
async ({ sessionId, idempotencyKey, reason }) => {
|
|
1763
|
+
const authorization = await authorizeFirstPartySession(
|
|
1764
|
+
deps,
|
|
1765
|
+
grant,
|
|
1766
|
+
sessionId,
|
|
1767
|
+
"session.control",
|
|
1768
|
+
);
|
|
1516
1769
|
if (callerSessionId !== null) {
|
|
1517
1770
|
const controlled = await controlAgentSessionWorkstream(
|
|
1518
1771
|
deps,
|
|
@@ -1526,27 +1779,37 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1526
1779
|
);
|
|
1527
1780
|
return json({
|
|
1528
1781
|
receiptId: controlled.receipt.id,
|
|
1529
|
-
effectiveControl:
|
|
1782
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1783
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
1784
|
+
sessionId,
|
|
1785
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1786
|
+
),
|
|
1530
1787
|
interruptionCount: controlled.interruptionCount,
|
|
1531
1788
|
replay: controlled.replay,
|
|
1532
1789
|
});
|
|
1533
1790
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
},
|
|
1548
|
-
),
|
|
1791
|
+
const controlled = await controlHumanSessionWorkstream(
|
|
1792
|
+
deps,
|
|
1793
|
+
{
|
|
1794
|
+
accountId: grant.accountId,
|
|
1795
|
+
workspaceId: grant.workspaceId,
|
|
1796
|
+
sessionId,
|
|
1797
|
+
subjectId: grant.subjectId,
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
action: "resume",
|
|
1801
|
+
clientEventId: idempotencyKey,
|
|
1802
|
+
...(reason ? { reason } : {}),
|
|
1803
|
+
},
|
|
1549
1804
|
);
|
|
1805
|
+
return json({
|
|
1806
|
+
...controlled,
|
|
1807
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1808
|
+
controlled.effectiveControl,
|
|
1809
|
+
sessionId,
|
|
1810
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1811
|
+
),
|
|
1812
|
+
});
|
|
1550
1813
|
},
|
|
1551
1814
|
);
|
|
1552
1815
|
|
|
@@ -1563,6 +1826,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1563
1826
|
},
|
|
1564
1827
|
},
|
|
1565
1828
|
async ({ sessionId, instruction, idempotencyKey }) => {
|
|
1829
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.steer");
|
|
1566
1830
|
const result = await steerAgentSession(
|
|
1567
1831
|
deps,
|
|
1568
1832
|
exactAgentCommandContext(grant, callerSessionId),
|
|
@@ -1590,15 +1854,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1590
1854
|
},
|
|
1591
1855
|
},
|
|
1592
1856
|
async ({ session_id, title }) => {
|
|
1857
|
+
await authorizeFirstPartySession(deps, grant, session_id, "session.title.write");
|
|
1593
1858
|
await requireSession(deps.db, grant.workspaceId, session_id);
|
|
1594
|
-
const result = await updateSessionTitle(
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
title,
|
|
1599
|
-
|
|
1600
|
-
);
|
|
1601
|
-
return json({ ok: true, updated: result.updated, title: result.title ?? title });
|
|
1859
|
+
const result = await updateSessionTitle(deps, grant, session_id, title, "agent");
|
|
1860
|
+
return json({
|
|
1861
|
+
ok: true,
|
|
1862
|
+
updated: result.updated,
|
|
1863
|
+
title: result.title ?? title,
|
|
1864
|
+
});
|
|
1602
1865
|
},
|
|
1603
1866
|
);
|
|
1604
1867
|
}
|
|
@@ -1703,7 +1966,11 @@ function registerVariableSetTools(
|
|
|
1703
1966
|
variableSetId: variableSet.id,
|
|
1704
1967
|
variableName: parsedName.data,
|
|
1705
1968
|
});
|
|
1706
|
-
const responseVariableSet = {
|
|
1969
|
+
const responseVariableSet = {
|
|
1970
|
+
id: variableSet.id,
|
|
1971
|
+
name: variableSet.name,
|
|
1972
|
+
created,
|
|
1973
|
+
};
|
|
1707
1974
|
return json({
|
|
1708
1975
|
variableSet: responseVariableSet,
|
|
1709
1976
|
environment: responseVariableSet,
|
|
@@ -1750,22 +2017,15 @@ function registerVariableSetTools(
|
|
|
1750
2017
|
}
|
|
1751
2018
|
}
|
|
1752
2019
|
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1756
|
-
|
|
1757
|
-
function registerGitHubConnectTool(
|
|
1758
|
-
server: McpServer,
|
|
1759
|
-
deps: ApiRouteDeps,
|
|
1760
|
-
grant: AccessGrant,
|
|
1761
|
-
options: McpServerOptions,
|
|
1762
|
-
json: JsonResult,
|
|
1763
|
-
): void {
|
|
2020
|
+
// The tool remains registered for compatibility, but every new installation
|
|
2021
|
+
// binding entry point is fail-closed until a provider-supported authority
|
|
2022
|
+
// proof stronger than installation visibility is available.
|
|
2023
|
+
function registerGitHubConnectTool(server: McpServer, deps: ApiRouteDeps, json: JsonResult): void {
|
|
1764
2024
|
server.registerTool(
|
|
1765
2025
|
"github_connect_link",
|
|
1766
2026
|
{
|
|
1767
2027
|
description:
|
|
1768
|
-
"
|
|
2028
|
+
"Report GitHub App connection availability. New installation binding is disabled until GitHub installation authority can be proven, so installUrl and linkUrl are null.",
|
|
1769
2029
|
inputSchema: {},
|
|
1770
2030
|
},
|
|
1771
2031
|
async () => {
|
|
@@ -1773,28 +2033,19 @@ function registerGitHubConnectTool(
|
|
|
1773
2033
|
const missing = githubAppMissingSettings(settings);
|
|
1774
2034
|
const slug = settings.githubAppSlug?.trim() || null;
|
|
1775
2035
|
if (missing.length > 0 || !slug) {
|
|
1776
|
-
return json({
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
).replace(/\/+$/, "");
|
|
1784
|
-
if (!base) {
|
|
1785
|
-
throw new Error(
|
|
1786
|
-
"github_connect_link requires OPENGENI_PUBLIC_BASE_URL (or OPENGENI_GITHUB_APP_MANIFEST_BASE_URL) so the install link can route through this deployment",
|
|
1787
|
-
);
|
|
2036
|
+
return json({
|
|
2037
|
+
configured: false,
|
|
2038
|
+
appSlug: slug,
|
|
2039
|
+
installUrl: null,
|
|
2040
|
+
linkUrl: null,
|
|
2041
|
+
missing,
|
|
2042
|
+
});
|
|
1788
2043
|
}
|
|
1789
|
-
const state = createSignedState(deps.githubStateSecret, {
|
|
1790
|
-
accountId: grant.accountId,
|
|
1791
|
-
workspaceId: grant.workspaceId,
|
|
1792
|
-
});
|
|
1793
2044
|
return json({
|
|
1794
2045
|
configured: true,
|
|
1795
2046
|
appSlug: slug,
|
|
1796
|
-
installUrl:
|
|
1797
|
-
|
|
2047
|
+
installUrl: null,
|
|
2048
|
+
linkUrl: null,
|
|
1798
2049
|
missing: [],
|
|
1799
2050
|
});
|
|
1800
2051
|
},
|
|
@@ -1845,9 +2096,20 @@ function registerGitHubTokenTool(
|
|
|
1845
2096
|
if (selected.some((item) => item.installationId !== installationId)) {
|
|
1846
2097
|
throw new Error("GitHub App repository resources must belong to one installation");
|
|
1847
2098
|
}
|
|
2099
|
+
const repositoryIds = selected.map((item) => item.repositoryId);
|
|
2100
|
+
if (
|
|
2101
|
+
!(await areGitHubRepositoriesAllowedForWorkspace(
|
|
2102
|
+
deps.db,
|
|
2103
|
+
grant.workspaceId,
|
|
2104
|
+
installationId,
|
|
2105
|
+
repositoryIds,
|
|
2106
|
+
))
|
|
2107
|
+
) {
|
|
2108
|
+
throw new Error("this workspace no longer authorizes the session's GitHub repositories");
|
|
2109
|
+
}
|
|
1848
2110
|
const token = await createGitHubAppInstallationToken(deps.settings, {
|
|
1849
2111
|
installationId,
|
|
1850
|
-
repositoryIds
|
|
2112
|
+
repositoryIds,
|
|
1851
2113
|
});
|
|
1852
2114
|
return json({
|
|
1853
2115
|
token,
|
|
@@ -1881,9 +2143,12 @@ function repositoryWithScheduledTaskResource(
|
|
|
1881
2143
|
kind: "repository",
|
|
1882
2144
|
uri,
|
|
1883
2145
|
ref: repository.defaultBranch,
|
|
1884
|
-
mountPath:
|
|
2146
|
+
mountPath: defaultRepositoryMountPath(uri),
|
|
1885
2147
|
...(repository.private
|
|
1886
|
-
? {
|
|
2148
|
+
? {
|
|
2149
|
+
githubInstallationId: repository.installationId,
|
|
2150
|
+
githubRepositoryId: repository.id,
|
|
2151
|
+
}
|
|
1887
2152
|
: {}),
|
|
1888
2153
|
},
|
|
1889
2154
|
};
|
|
@@ -1892,12 +2157,7 @@ function repositoryWithScheduledTaskResource(
|
|
|
1892
2157
|
function normalizedRepositoryUri(value: string): string {
|
|
1893
2158
|
const url = new URL(value);
|
|
1894
2159
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1895
|
-
return `https://${url.
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
function repositoryMountPath(uri: string): string {
|
|
1899
|
-
const url = new URL(uri);
|
|
1900
|
-
return `repos/${url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "")}`;
|
|
2160
|
+
return `https://${url.host.toLowerCase()}/${path}.git`;
|
|
1901
2161
|
}
|
|
1902
2162
|
|
|
1903
2163
|
function boundedMcpLimit(limit: number | undefined): number {
|
|
@@ -1907,6 +2167,416 @@ function boundedMcpLimit(limit: number | undefined): number {
|
|
|
1907
2167
|
return Math.min(500, Math.max(1, Math.floor(limit)));
|
|
1908
2168
|
}
|
|
1909
2169
|
|
|
2170
|
+
function boundedRigHistoryLimit(limit: number | undefined): number {
|
|
2171
|
+
if (!limit || !Number.isFinite(limit)) return 20;
|
|
2172
|
+
return Math.min(100, Math.max(1, Math.floor(limit)));
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function boundedSessionEventMcpLimit(limit: number | undefined): number {
|
|
2176
|
+
if (!limit || !Number.isFinite(limit)) return 40;
|
|
2177
|
+
return Math.min(250, Math.max(1, Math.floor(limit)));
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const SESSION_DISCOVERY_DEFAULT_LIMIT = 20;
|
|
2181
|
+
const SESSION_DISCOVERY_MAX_LIMIT = 100;
|
|
2182
|
+
const SESSION_DISCOVERY_TEXT_CHARS = 600;
|
|
2183
|
+
const SESSION_DISCOVERY_PREVIEW_MAX_BYTES = 16_384;
|
|
2184
|
+
const SESSION_DISCOVERY_PREVIEW_OMISSION_REASON = "aggregatePreviewBudget" as const;
|
|
2185
|
+
const SESSION_DISCOVERY_PAGE_MAX_BYTES = 128_000;
|
|
2186
|
+
const SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL = "session_events" as const;
|
|
2187
|
+
const SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT = {
|
|
2188
|
+
direction: "before",
|
|
2189
|
+
limit: 1,
|
|
2190
|
+
mode: "monitoring",
|
|
2191
|
+
payloadMode: "summary",
|
|
2192
|
+
} as const;
|
|
2193
|
+
|
|
2194
|
+
function sessionDiscoveryPreviewDrillDownInput(sessionId: string, type: SessionEventType) {
|
|
2195
|
+
return {
|
|
2196
|
+
sessionId,
|
|
2197
|
+
includeTypes: [type],
|
|
2198
|
+
...SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT,
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function boundedSessionDiscoveryLimit(limit: number | undefined): number {
|
|
2203
|
+
if (!limit || !Number.isFinite(limit)) return SESSION_DISCOVERY_DEFAULT_LIMIT;
|
|
2204
|
+
return Math.min(SESSION_DISCOVERY_MAX_LIMIT, Math.max(1, Math.floor(limit)));
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
export function encodeSessionDiscoveryCursor(cursor: SessionDiscoveryCursor): string {
|
|
2208
|
+
return Buffer.from(
|
|
2209
|
+
JSON.stringify({
|
|
2210
|
+
v: 2,
|
|
2211
|
+
orderBy: cursor.orderBy,
|
|
2212
|
+
sortRevision: cursor.sortRevision,
|
|
2213
|
+
sortAt: cursor.sortAt,
|
|
2214
|
+
id: cursor.id,
|
|
2215
|
+
snapshotAt: cursor.snapshotAt,
|
|
2216
|
+
snapshotRevision: cursor.snapshotRevision,
|
|
2217
|
+
updatedAfter: cursor.updatedAfter,
|
|
2218
|
+
}),
|
|
2219
|
+
"utf8",
|
|
2220
|
+
).toString("base64url");
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const SESSION_DISCOVERY_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
|
|
2224
|
+
const SESSION_DISCOVERY_REVISION = /^(?:0|[1-9]\d*)$/;
|
|
2225
|
+
const SESSION_DISCOVERY_REVISION_MAX = 9_223_372_036_854_775_807n;
|
|
2226
|
+
const SESSION_DISCOVERY_UUID =
|
|
2227
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2228
|
+
|
|
2229
|
+
function normalizeSessionDiscoveryTimestamp(value: string, label: string): string {
|
|
2230
|
+
if (!SESSION_DISCOVERY_TIMESTAMP.test(value) || Number.isNaN(new Date(value).getTime())) {
|
|
2231
|
+
throw new Error(`sessions_list ${label} must be an ISO UTC date-time`);
|
|
2232
|
+
}
|
|
2233
|
+
// Preserve up to six fractional digits. Converting through JS Date would
|
|
2234
|
+
// discard PostgreSQL microseconds and can skip equal-millisecond rows.
|
|
2235
|
+
return value;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
function normalizeSessionDiscoveryRevision(value: string, label: string): string {
|
|
2239
|
+
if (!SESSION_DISCOVERY_REVISION.test(value)) {
|
|
2240
|
+
throw new Error(`sessions_list ${label} must be a decimal activity revision`);
|
|
2241
|
+
}
|
|
2242
|
+
const revision = BigInt(value);
|
|
2243
|
+
if (revision > SESSION_DISCOVERY_REVISION_MAX) {
|
|
2244
|
+
throw new Error(`sessions_list ${label} exceeds the database activity revision range`);
|
|
2245
|
+
}
|
|
2246
|
+
return revision.toString();
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCursor {
|
|
2250
|
+
try {
|
|
2251
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as {
|
|
2252
|
+
v?: unknown;
|
|
2253
|
+
orderBy?: unknown;
|
|
2254
|
+
sortRevision?: unknown;
|
|
2255
|
+
sortAt?: unknown;
|
|
2256
|
+
createdAt?: unknown;
|
|
2257
|
+
id?: unknown;
|
|
2258
|
+
snapshotAt?: unknown;
|
|
2259
|
+
snapshotRevision?: unknown;
|
|
2260
|
+
updatedAfter?: unknown;
|
|
2261
|
+
};
|
|
2262
|
+
if (
|
|
2263
|
+
parsed.v === undefined &&
|
|
2264
|
+
typeof parsed.createdAt === "string" &&
|
|
2265
|
+
typeof parsed.id === "string" &&
|
|
2266
|
+
SESSION_DISCOVERY_UUID.test(parsed.id)
|
|
2267
|
+
) {
|
|
2268
|
+
const createdAt = normalizeSessionDiscoveryTimestamp(
|
|
2269
|
+
parsed.createdAt,
|
|
2270
|
+
"legacy cursor createdAt",
|
|
2271
|
+
);
|
|
2272
|
+
return {
|
|
2273
|
+
orderBy: "createdAt",
|
|
2274
|
+
sortRevision: "0",
|
|
2275
|
+
sortAt: createdAt,
|
|
2276
|
+
id: parsed.id,
|
|
2277
|
+
snapshotAt: createdAt,
|
|
2278
|
+
snapshotRevision: "0",
|
|
2279
|
+
updatedAfter: null,
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
// The timestamp-fenced v1 format was never safe for updated-order
|
|
2283
|
+
// continuation. Preserve rolling compatibility only for creation cursors,
|
|
2284
|
+
// whose immutable ordering does not need an activity revision.
|
|
2285
|
+
if (
|
|
2286
|
+
parsed.v === 1 &&
|
|
2287
|
+
parsed.orderBy === "createdAt" &&
|
|
2288
|
+
typeof parsed.sortAt === "string" &&
|
|
2289
|
+
typeof parsed.snapshotAt === "string" &&
|
|
2290
|
+
parsed.updatedAfter === null &&
|
|
2291
|
+
typeof parsed.id === "string" &&
|
|
2292
|
+
SESSION_DISCOVERY_UUID.test(parsed.id)
|
|
2293
|
+
) {
|
|
2294
|
+
return {
|
|
2295
|
+
orderBy: "createdAt",
|
|
2296
|
+
sortRevision: "0",
|
|
2297
|
+
sortAt: normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt"),
|
|
2298
|
+
id: parsed.id,
|
|
2299
|
+
snapshotAt: normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt"),
|
|
2300
|
+
snapshotRevision: "0",
|
|
2301
|
+
updatedAfter: null,
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
if (
|
|
2305
|
+
parsed.v !== 2 ||
|
|
2306
|
+
(parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt") ||
|
|
2307
|
+
typeof parsed.sortRevision !== "string" ||
|
|
2308
|
+
typeof parsed.sortAt !== "string" ||
|
|
2309
|
+
typeof parsed.snapshotAt !== "string" ||
|
|
2310
|
+
typeof parsed.snapshotRevision !== "string" ||
|
|
2311
|
+
(parsed.updatedAfter !== null && typeof parsed.updatedAfter !== "string") ||
|
|
2312
|
+
typeof parsed.id !== "string" ||
|
|
2313
|
+
!SESSION_DISCOVERY_UUID.test(parsed.id)
|
|
2314
|
+
) {
|
|
2315
|
+
throw new Error("invalid cursor fields");
|
|
2316
|
+
}
|
|
2317
|
+
const sortAt = normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt");
|
|
2318
|
+
const snapshotAt = normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt");
|
|
2319
|
+
const sortRevision = normalizeSessionDiscoveryRevision(
|
|
2320
|
+
parsed.sortRevision,
|
|
2321
|
+
"cursor sortRevision",
|
|
2322
|
+
);
|
|
2323
|
+
const snapshotRevision = normalizeSessionDiscoveryRevision(
|
|
2324
|
+
parsed.snapshotRevision,
|
|
2325
|
+
"cursor snapshotRevision",
|
|
2326
|
+
);
|
|
2327
|
+
const normalizedUpdatedAfter =
|
|
2328
|
+
parsed.updatedAfter === null
|
|
2329
|
+
? null
|
|
2330
|
+
: normalizeSessionDiscoveryRevision(parsed.updatedAfter, "cursor updatedAfter");
|
|
2331
|
+
if (normalizedUpdatedAfter !== null && parsed.orderBy !== "updatedAt") {
|
|
2332
|
+
throw new Error("incremental cursor requires updatedAt order");
|
|
2333
|
+
}
|
|
2334
|
+
if (parsed.orderBy === "createdAt" && (sortRevision !== "0" || snapshotRevision !== "0")) {
|
|
2335
|
+
throw new Error("creation cursor cannot carry activity revisions");
|
|
2336
|
+
}
|
|
2337
|
+
return {
|
|
2338
|
+
orderBy: parsed.orderBy,
|
|
2339
|
+
sortRevision,
|
|
2340
|
+
sortAt,
|
|
2341
|
+
id: parsed.id,
|
|
2342
|
+
snapshotAt,
|
|
2343
|
+
snapshotRevision,
|
|
2344
|
+
updatedAfter: normalizedUpdatedAfter,
|
|
2345
|
+
};
|
|
2346
|
+
} catch {
|
|
2347
|
+
throw new Error("sessions_list cursor is invalid");
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function capSessionDiscoveryText(
|
|
2352
|
+
value: string | null,
|
|
2353
|
+
maxChars = SESSION_DISCOVERY_TEXT_CHARS,
|
|
2354
|
+
originalChars?: number | null,
|
|
2355
|
+
) {
|
|
2356
|
+
if (value === null) {
|
|
2357
|
+
return { text: value, truncated: false };
|
|
2358
|
+
}
|
|
2359
|
+
const projectedChars = Array.from(value);
|
|
2360
|
+
const sourceChars = Math.max(projectedChars.length, originalChars ?? projectedChars.length);
|
|
2361
|
+
if (sourceChars <= maxChars) {
|
|
2362
|
+
return { text: value, truncated: false };
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// The database supplies only a bounded prefix plus the original character
|
|
2366
|
+
// count. Iterate to a stable marker width so the reported omission includes
|
|
2367
|
+
// the characters replaced by the marker itself.
|
|
2368
|
+
let bodyChars = maxChars;
|
|
2369
|
+
let marker = "";
|
|
2370
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
2371
|
+
const omittedChars = Math.max(0, sourceChars - bodyChars);
|
|
2372
|
+
marker = `…[${omittedChars} chars truncated]…`;
|
|
2373
|
+
const nextBodyChars = Math.max(0, maxChars - Array.from(marker).length);
|
|
2374
|
+
if (nextBodyChars === bodyChars) break;
|
|
2375
|
+
bodyChars = nextBodyChars;
|
|
2376
|
+
}
|
|
2377
|
+
return {
|
|
2378
|
+
text: `${projectedChars.slice(0, bodyChars).join("")}${marker}`,
|
|
2379
|
+
truncated: true,
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
export function capSessionDiscoveryPage(
|
|
2384
|
+
page: Awaited<ReturnType<typeof listSessionDiscoverySummaries>>,
|
|
2385
|
+
includeLastMessage: boolean,
|
|
2386
|
+
) {
|
|
2387
|
+
const projected = page.sessions.map((session) => {
|
|
2388
|
+
const title = capSessionDiscoveryText(session.title, 200, session.titleOriginalChars);
|
|
2389
|
+
const goal = session.goal
|
|
2390
|
+
? capSessionDiscoveryText(
|
|
2391
|
+
session.goal.text,
|
|
2392
|
+
SESSION_DISCOVERY_TEXT_CHARS,
|
|
2393
|
+
session.goal.textOriginalChars,
|
|
2394
|
+
)
|
|
2395
|
+
: null;
|
|
2396
|
+
const preview = includeLastMessage
|
|
2397
|
+
? capSessionDiscoveryText(
|
|
2398
|
+
session.latestMessage?.preview ?? null,
|
|
2399
|
+
SESSION_DISCOVERY_TEXT_CHARS,
|
|
2400
|
+
session.latestMessage?.previewOriginalChars,
|
|
2401
|
+
)
|
|
2402
|
+
: null;
|
|
2403
|
+
const blocker = session.effectiveControl.primaryBlocker;
|
|
2404
|
+
const blockerDisplayName = blocker
|
|
2405
|
+
? capSessionDiscoveryText(blocker.displayName, 200, blocker.displayNameOriginalChars)
|
|
2406
|
+
: null;
|
|
2407
|
+
return {
|
|
2408
|
+
id: session.id,
|
|
2409
|
+
title: title.text,
|
|
2410
|
+
titleTruncated: title.truncated,
|
|
2411
|
+
parentSessionId: session.parentSessionId,
|
|
2412
|
+
isRoot: session.parentSessionId === null,
|
|
2413
|
+
status: session.status,
|
|
2414
|
+
pause: {
|
|
2415
|
+
state: session.effectiveControl.state,
|
|
2416
|
+
additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
|
|
2417
|
+
source: blocker
|
|
2418
|
+
? {
|
|
2419
|
+
kind: blocker.kind,
|
|
2420
|
+
...(blocker.sessionId ? { sessionId: blocker.sessionId } : {}),
|
|
2421
|
+
displayName: blockerDisplayName!.text,
|
|
2422
|
+
displayNameTruncated: blockerDisplayName!.truncated,
|
|
2423
|
+
}
|
|
2424
|
+
: null,
|
|
2425
|
+
},
|
|
2426
|
+
goal: session.goal
|
|
2427
|
+
? {
|
|
2428
|
+
status: session.goal.status,
|
|
2429
|
+
summary: goal!.text,
|
|
2430
|
+
summaryTruncated: goal!.truncated,
|
|
2431
|
+
}
|
|
2432
|
+
: null,
|
|
2433
|
+
queuedPromptCount: session.queuedPromptCount,
|
|
2434
|
+
children: session.treeStats,
|
|
2435
|
+
...(includeLastMessage
|
|
2436
|
+
? {
|
|
2437
|
+
latestMessage: session.latestMessage
|
|
2438
|
+
? {
|
|
2439
|
+
type: session.latestMessage.type,
|
|
2440
|
+
preview: preview!.text,
|
|
2441
|
+
previewTruncated: preview!.truncated,
|
|
2442
|
+
}
|
|
2443
|
+
: null,
|
|
2444
|
+
}
|
|
2445
|
+
: {}),
|
|
2446
|
+
createdAt: session.createdAt,
|
|
2447
|
+
updatedAt: session.updatedAt,
|
|
2448
|
+
};
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
// The database order is already the useful discovery order. Spend the
|
|
2452
|
+
// separate preview budget in that order so the first page retains previews
|
|
2453
|
+
// for the most relevant rows and later rows remain discoverable by status.
|
|
2454
|
+
let budgetBytes = 0;
|
|
2455
|
+
const budgeted = includeLastMessage
|
|
2456
|
+
? projected.map((session) => {
|
|
2457
|
+
const latestMessage = session.latestMessage;
|
|
2458
|
+
if (!latestMessage || latestMessage.preview === null) return session;
|
|
2459
|
+
const candidateBytes = Buffer.byteLength(latestMessage.preview, "utf8");
|
|
2460
|
+
if (budgetBytes + candidateBytes <= SESSION_DISCOVERY_PREVIEW_MAX_BYTES) {
|
|
2461
|
+
budgetBytes += candidateBytes;
|
|
2462
|
+
return session;
|
|
2463
|
+
}
|
|
2464
|
+
return {
|
|
2465
|
+
...session,
|
|
2466
|
+
latestMessage: {
|
|
2467
|
+
...latestMessage,
|
|
2468
|
+
preview: null,
|
|
2469
|
+
previewOmitted: true,
|
|
2470
|
+
previewOmissionReason: SESSION_DISCOVERY_PREVIEW_OMISSION_REASON,
|
|
2471
|
+
previewDrillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
|
|
2472
|
+
previewDrillDownInput: sessionDiscoveryPreviewDrillDownInput(
|
|
2473
|
+
session.id,
|
|
2474
|
+
latestMessage.type,
|
|
2475
|
+
),
|
|
2476
|
+
},
|
|
2477
|
+
};
|
|
2478
|
+
})
|
|
2479
|
+
: projected;
|
|
2480
|
+
|
|
2481
|
+
let kept = budgeted;
|
|
2482
|
+
const build = () => {
|
|
2483
|
+
const previewBytes = includeLastMessage
|
|
2484
|
+
? kept.reduce(
|
|
2485
|
+
(total, session) =>
|
|
2486
|
+
total +
|
|
2487
|
+
(session.latestMessage?.preview === null || !session.latestMessage
|
|
2488
|
+
? 0
|
|
2489
|
+
: Buffer.byteLength(session.latestMessage.preview, "utf8")),
|
|
2490
|
+
0,
|
|
2491
|
+
)
|
|
2492
|
+
: 0;
|
|
2493
|
+
const previewOmittedCount = includeLastMessage
|
|
2494
|
+
? kept.filter((session) => {
|
|
2495
|
+
const latestMessage = session.latestMessage;
|
|
2496
|
+
return (
|
|
2497
|
+
latestMessage != null &&
|
|
2498
|
+
"previewOmitted" in latestMessage &&
|
|
2499
|
+
latestMessage.previewOmitted === true
|
|
2500
|
+
);
|
|
2501
|
+
}).length
|
|
2502
|
+
: 0;
|
|
2503
|
+
const lastKept = kept.at(-1);
|
|
2504
|
+
const droppedForByteCap = kept.length < projected.length;
|
|
2505
|
+
const sourceLast = lastKept
|
|
2506
|
+
? page.sessions.find((session) => session.id === lastKept.id)
|
|
2507
|
+
: undefined;
|
|
2508
|
+
const nextCursor = droppedForByteCap
|
|
2509
|
+
? sourceLast
|
|
2510
|
+
? encodeSessionDiscoveryCursor({
|
|
2511
|
+
orderBy: page.orderBy,
|
|
2512
|
+
sortRevision: sourceLast.sortRevision,
|
|
2513
|
+
sortAt: sourceLast.sortAt,
|
|
2514
|
+
id: sourceLast.id,
|
|
2515
|
+
snapshotAt: page.snapshotAt,
|
|
2516
|
+
snapshotRevision: page.snapshotRevision,
|
|
2517
|
+
updatedAfter: page.updatedAfter,
|
|
2518
|
+
})
|
|
2519
|
+
: null
|
|
2520
|
+
: page.nextCursor
|
|
2521
|
+
? encodeSessionDiscoveryCursor(page.nextCursor)
|
|
2522
|
+
: null;
|
|
2523
|
+
const result = {
|
|
2524
|
+
sessions: kept,
|
|
2525
|
+
total: page.total,
|
|
2526
|
+
hasMore: page.hasMore || droppedForByteCap,
|
|
2527
|
+
nextCursor,
|
|
2528
|
+
orderBy: page.orderBy,
|
|
2529
|
+
snapshotAt: page.snapshotAt,
|
|
2530
|
+
snapshotRevision: page.snapshotRevision,
|
|
2531
|
+
updatedAfter: page.updatedAfter,
|
|
2532
|
+
updatedThrough: page.updatedThrough,
|
|
2533
|
+
...(includeLastMessage
|
|
2534
|
+
? {
|
|
2535
|
+
latestMessagePreviewBudget: {
|
|
2536
|
+
bytes: previewBytes,
|
|
2537
|
+
maxBytes: SESSION_DISCOVERY_PREVIEW_MAX_BYTES,
|
|
2538
|
+
omittedCount: previewOmittedCount,
|
|
2539
|
+
truncated: previewOmittedCount > 0,
|
|
2540
|
+
omissionReason:
|
|
2541
|
+
previewOmittedCount > 0 ? SESSION_DISCOVERY_PREVIEW_OMISSION_REASON : null,
|
|
2542
|
+
drillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
|
|
2543
|
+
drillDownInput: {
|
|
2544
|
+
includeTypes: ["user.message", "agent.message.completed"] as const,
|
|
2545
|
+
...SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT,
|
|
2546
|
+
},
|
|
2547
|
+
},
|
|
2548
|
+
}
|
|
2549
|
+
: {}),
|
|
2550
|
+
responseTruncated: droppedForByteCap,
|
|
2551
|
+
...(droppedForByteCap
|
|
2552
|
+
? {
|
|
2553
|
+
truncationReason: `response exceeded ${SESSION_DISCOVERY_PAGE_MAX_BYTES} bytes; continue with nextCursor`,
|
|
2554
|
+
}
|
|
2555
|
+
: {}),
|
|
2556
|
+
bytes: 0,
|
|
2557
|
+
maxBytes: SESSION_DISCOVERY_PAGE_MAX_BYTES,
|
|
2558
|
+
};
|
|
2559
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
2560
|
+
const measured = Buffer.byteLength(JSON.stringify(result, null, 2), "utf8");
|
|
2561
|
+
if (result.bytes === measured) break;
|
|
2562
|
+
result.bytes = measured;
|
|
2563
|
+
}
|
|
2564
|
+
return result;
|
|
2565
|
+
};
|
|
2566
|
+
|
|
2567
|
+
let result = build();
|
|
2568
|
+
while (result.bytes > SESSION_DISCOVERY_PAGE_MAX_BYTES && kept.length > 1) {
|
|
2569
|
+
kept = kept.slice(0, -1);
|
|
2570
|
+
result = build();
|
|
2571
|
+
}
|
|
2572
|
+
if (result.bytes > SESSION_DISCOVERY_PAGE_MAX_BYTES) {
|
|
2573
|
+
throw new RangeError(
|
|
2574
|
+
`sessions_list metadata exceeds its ${SESSION_DISCOVERY_PAGE_MAX_BYTES}-byte envelope`,
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
return result;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
1910
2580
|
function parseMcpDate(raw: string, label: string): Date {
|
|
1911
2581
|
const date = new Date(raw);
|
|
1912
2582
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -1914,3 +2584,15 @@ function parseMcpDate(raw: string, label: string): Date {
|
|
|
1914
2584
|
}
|
|
1915
2585
|
return date;
|
|
1916
2586
|
}
|
|
2587
|
+
|
|
2588
|
+
async function withMcpEffectivePolicy(
|
|
2589
|
+
deps: ApiRouteDeps,
|
|
2590
|
+
workspaceId: string,
|
|
2591
|
+
session: Session,
|
|
2592
|
+
): Promise<Session> {
|
|
2593
|
+
const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
|
|
2594
|
+
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings),
|
|
2595
|
+
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings),
|
|
2596
|
+
]);
|
|
2597
|
+
return sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds);
|
|
2598
|
+
}
|