@opengeni/api-router 0.7.3 → 0.11.1
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.js +1 -1
- package/dist/{chunk-EYYTFA7N.js → chunk-7PQKPKW5.js} +4117 -1863
- package/dist/chunk-7PQKPKW5.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +12 -11
- package/src/app.ts +37 -15
- package/src/codex-redemption-security.ts +96 -0
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +151 -83
- package/src/mcp/session-view.ts +102 -1
- package/src/mcp/toolspace.ts +389 -124
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +866 -14
- package/src/routes/files.ts +153 -0
- package/src/routes/machines.ts +22 -2
- package/src/routes/sessions.ts +637 -152
- package/src/routes/workspaces.ts +36 -2
- package/src/sandbox/channel-a.ts +54 -59
- package/src/sandbox/machines.ts +7 -0
- package/src/sandbox/rematerialize.ts +287 -0
- package/src/sandbox/viewer.ts +207 -129
- package/dist/chunk-EYYTFA7N.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -2,11 +2,15 @@ import {
|
|
|
2
2
|
CreateScheduledTaskRequest,
|
|
3
3
|
defaultRepositoryMountPath,
|
|
4
4
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
5
|
+
SessionEventLatestClass,
|
|
5
6
|
SessionEventPayloadMode,
|
|
6
7
|
SessionEventReadDirection,
|
|
7
8
|
SessionEventReadMode,
|
|
9
|
+
SessionEventResultMode,
|
|
8
10
|
SessionEventSemanticClass,
|
|
9
11
|
SessionEventType,
|
|
12
|
+
compactSessionEventResult,
|
|
13
|
+
sessionEventLatestClassToSemanticClass,
|
|
10
14
|
SessionMcpCredentialUpdateInput,
|
|
11
15
|
VariableSetVariableName,
|
|
12
16
|
type AccessGrant,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
type Permission,
|
|
15
19
|
type ResourceRef,
|
|
16
20
|
type SessionAuthorizationOperation,
|
|
21
|
+
type Session,
|
|
17
22
|
UpdateScheduledTaskRequest,
|
|
18
23
|
} from "@opengeni/contracts";
|
|
19
24
|
import {
|
|
@@ -53,15 +58,15 @@ import {
|
|
|
53
58
|
saveWorkspaceMemory,
|
|
54
59
|
searchWorkspaceMemories,
|
|
55
60
|
serializeEffectiveSessionControl,
|
|
56
|
-
|
|
61
|
+
setSessionGoalStatusWithEvent,
|
|
57
62
|
setVariableSetVariable,
|
|
58
63
|
updateScheduledTask,
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
updateSessionGoalWithEvent,
|
|
65
|
+
upsertSessionGoalWithEvent,
|
|
61
66
|
RigChangeAlreadyVerifyingError,
|
|
62
67
|
RigChangeTransitionError,
|
|
63
68
|
} from "@opengeni/db";
|
|
64
|
-
import { appendAndPublishEvents } from "@opengeni/events";
|
|
69
|
+
import { appendAndPublishEvents, publishDurableSessionEvents } from "@opengeni/events";
|
|
65
70
|
import {
|
|
66
71
|
createGitHubAppInstallationToken,
|
|
67
72
|
GitHubAppConfigurationError,
|
|
@@ -104,9 +109,14 @@ import {
|
|
|
104
109
|
controlAgentSessionWorkstream,
|
|
105
110
|
controlHumanSessionWorkstream,
|
|
106
111
|
createSessionForRequest,
|
|
112
|
+
SessionSpawnDeniedError,
|
|
113
|
+
sessionSpawnDenialEnvelope,
|
|
107
114
|
sendAgentSessionMessage,
|
|
108
115
|
steerAgentSession,
|
|
109
116
|
updateSessionTitle,
|
|
117
|
+
sessionWithEffectiveToolPolicy,
|
|
118
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
119
|
+
workspaceSessionToolPolicyServerIds,
|
|
110
120
|
type AgentSessionCommandContext,
|
|
111
121
|
} from "@opengeni/core";
|
|
112
122
|
import {
|
|
@@ -120,12 +130,14 @@ import {
|
|
|
120
130
|
type RunOnOp,
|
|
121
131
|
} from "@opengeni/core";
|
|
122
132
|
import {
|
|
133
|
+
boundSessionEventCompactResult,
|
|
123
134
|
boundSessionEventMcpPage,
|
|
124
135
|
boundSessionDetailMcp,
|
|
125
136
|
boundRigDetailMcp,
|
|
126
137
|
SESSION_EVENT_MCP_MAX_BYTES,
|
|
127
138
|
} from "./session-view";
|
|
128
139
|
import type { ToolspaceMcpSurface } from "./toolspace";
|
|
140
|
+
import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
|
|
129
141
|
|
|
130
142
|
export type McpServerOptions = {
|
|
131
143
|
// Origin of the HTTP request that reached the MCP route. Retained in the
|
|
@@ -730,7 +742,7 @@ function registerGoalTools(
|
|
|
730
742
|
? (grant.metadata["turnId"] as string)
|
|
731
743
|
: null;
|
|
732
744
|
await assertGoalReactivationAllowed(deps, grant.workspaceId, sessionId, callerTurnId);
|
|
733
|
-
const { goal,
|
|
745
|
+
const { goal, events } = await upsertSessionGoalWithEvent(deps.db, {
|
|
734
746
|
accountId: grant.accountId,
|
|
735
747
|
workspaceId: grant.workspaceId,
|
|
736
748
|
sessionId,
|
|
@@ -738,20 +750,11 @@ function registerGoalTools(
|
|
|
738
750
|
successCriteria: successCriteria ?? null,
|
|
739
751
|
maxAutoContinuations: maxAutoContinuations ?? null,
|
|
740
752
|
createdBy: "agent",
|
|
753
|
+
actor: "agent",
|
|
741
754
|
});
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
payload: {
|
|
746
|
-
goalId: goal.id,
|
|
747
|
-
text: goal.text,
|
|
748
|
-
...(goal.successCriteria ? { successCriteria: goal.successCriteria } : {}),
|
|
749
|
-
version: goal.version,
|
|
750
|
-
actor: "agent",
|
|
751
|
-
replaced,
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
]);
|
|
755
|
+
if (events.length > 0) {
|
|
756
|
+
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
757
|
+
}
|
|
755
758
|
return json(goal);
|
|
756
759
|
},
|
|
757
760
|
);
|
|
@@ -765,36 +768,36 @@ function registerGoalTools(
|
|
|
765
768
|
text: z4.string().min(1).optional(),
|
|
766
769
|
successCriteria: z4.string().min(1).optional(),
|
|
767
770
|
progressNote: z4.string().min(1).optional(),
|
|
771
|
+
idempotencyKey: z4.string().uuid(),
|
|
768
772
|
},
|
|
769
773
|
},
|
|
770
|
-
async ({ text, successCriteria, progressNote }) => {
|
|
774
|
+
async ({ text, successCriteria, progressNote, idempotencyKey }) => {
|
|
771
775
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (existing.status === "completed") {
|
|
778
|
-
throw new Error("session goal is completed; use goal_set to start a new goal");
|
|
779
|
-
}
|
|
780
|
-
const goal = await updateSessionGoal(deps.db, grant.workspaceId, sessionId, {
|
|
781
|
-
...(text !== undefined ? { text } : {}),
|
|
782
|
-
...(successCriteria !== undefined ? { successCriteria } : {}),
|
|
783
|
-
});
|
|
784
|
-
await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
|
|
776
|
+
const context = exactAgentCommandContext(grant, sessionId);
|
|
777
|
+
const { goal, events, operationId, replay } = await updateSessionGoalWithEvent(
|
|
778
|
+
deps.db,
|
|
779
|
+
grant.workspaceId,
|
|
780
|
+
sessionId,
|
|
785
781
|
{
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
782
|
+
...(text !== undefined ? { text } : {}),
|
|
783
|
+
...(successCriteria !== undefined ? { successCriteria } : {}),
|
|
784
|
+
...(progressNote !== undefined ? { progressNote } : {}),
|
|
785
|
+
actor: "agent",
|
|
786
|
+
command: {
|
|
787
|
+
accountId: grant.accountId,
|
|
788
|
+
actor: {
|
|
789
|
+
type: "agent_attempt",
|
|
790
|
+
attemptId: context.callerAttemptId,
|
|
791
|
+
sessionId: context.callerSessionId,
|
|
792
|
+
turnId: context.callerTurnId,
|
|
793
|
+
executionGeneration: context.callerExecutionGeneration,
|
|
794
|
+
},
|
|
795
|
+
operationKey: idempotencyKey,
|
|
794
796
|
},
|
|
795
797
|
},
|
|
796
|
-
|
|
797
|
-
|
|
798
|
+
);
|
|
799
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
800
|
+
return json({ ...goal, operationId, replay });
|
|
798
801
|
},
|
|
799
802
|
);
|
|
800
803
|
|
|
@@ -812,17 +815,18 @@ function registerGoalTools(
|
|
|
812
815
|
if (!existing) {
|
|
813
816
|
throw new Error("this session has no goal; use goal_set first");
|
|
814
817
|
}
|
|
815
|
-
const { goal,
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
818
|
+
const { goal, events } = await setSessionGoalStatusWithEvent(
|
|
819
|
+
deps.db,
|
|
820
|
+
grant.workspaceId,
|
|
821
|
+
sessionId,
|
|
822
|
+
{
|
|
823
|
+
status: "completed",
|
|
824
|
+
evidence,
|
|
825
|
+
event: { type: "goal.completed", evidence },
|
|
826
|
+
},
|
|
827
|
+
);
|
|
828
|
+
if (events.length > 0) {
|
|
829
|
+
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
826
830
|
}
|
|
827
831
|
return json(goal);
|
|
828
832
|
},
|
|
@@ -842,25 +846,24 @@ function registerGoalTools(
|
|
|
842
846
|
if (!existing) {
|
|
843
847
|
throw new Error("this session has no goal; use goal_set first");
|
|
844
848
|
}
|
|
845
|
-
const { goal,
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
849
|
+
const { goal, events } = await setSessionGoalStatusWithEvent(
|
|
850
|
+
deps.db,
|
|
851
|
+
grant.workspaceId,
|
|
852
|
+
sessionId,
|
|
853
|
+
{
|
|
854
|
+
status: "paused",
|
|
855
|
+
rationale,
|
|
856
|
+
pausedReason: "agent",
|
|
857
|
+
event: {
|
|
853
858
|
type: "goal.paused",
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
reason: "agent",
|
|
858
|
-
rationale,
|
|
859
|
-
autoContinuations: goal.autoContinuations,
|
|
860
|
-
noProgressStreak: goal.noProgressStreak,
|
|
861
|
-
},
|
|
859
|
+
actor: "agent",
|
|
860
|
+
reason: "agent",
|
|
861
|
+
rationale,
|
|
862
862
|
},
|
|
863
|
-
|
|
863
|
+
},
|
|
864
|
+
);
|
|
865
|
+
if (events.length > 0) {
|
|
866
|
+
await deps.bus.publish(grant.workspaceId, sessionId, events);
|
|
864
867
|
}
|
|
865
868
|
return json(goal);
|
|
866
869
|
},
|
|
@@ -1029,6 +1032,13 @@ function registerFleetTools(
|
|
|
1029
1032
|
db: deps.db,
|
|
1030
1033
|
settings: deps.settings,
|
|
1031
1034
|
bus: deps.bus,
|
|
1035
|
+
ensureSessionGroupReady: async (ctx) => {
|
|
1036
|
+
const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
|
|
1037
|
+
return await ensureViewerSessionGroupReady(
|
|
1038
|
+
{ db: deps.db, settings: deps.settings, bus: deps.bus },
|
|
1039
|
+
{ accountId: ctx.accountId, workspaceId: ctx.workspaceId, session },
|
|
1040
|
+
);
|
|
1041
|
+
},
|
|
1032
1042
|
};
|
|
1033
1043
|
|
|
1034
1044
|
// Resolve the session's group sandbox (the default/home fleet member) at
|
|
@@ -1046,7 +1056,7 @@ function registerFleetTools(
|
|
|
1046
1056
|
"sandboxes_list",
|
|
1047
1057
|
{
|
|
1048
1058
|
description:
|
|
1049
|
-
"List the sandboxes this session can run on: its own session sandbox
|
|
1059
|
+
"List the sandboxes this session can run on: its own session sandbox plus enrolled selfhosted machines. `liveness` is conservative: online requires observed provider existence and verified workspace readiness. Provider, lease, route, archive, restore, workspace, lease epoch, and route epoch are also reported separately. Use an entry `id` as an attach/swap/run_on target.",
|
|
1050
1060
|
inputSchema: {},
|
|
1051
1061
|
},
|
|
1052
1062
|
async () => json(await listFleet(services, await fleetContext())),
|
|
@@ -1056,7 +1066,7 @@ function registerFleetTools(
|
|
|
1056
1066
|
"sandbox_attach",
|
|
1057
1067
|
{
|
|
1058
1068
|
description:
|
|
1059
|
-
'Attach this session to a sandbox
|
|
1069
|
+
'Attach this session to a sandbox for the next tool call. The target must be owned and verified ready. A same-target attach is a repair request: it revalidates readiness and advances the route epoch rather than returning unchanged success. Recovery-in-progress/degraded/unrecoverable outcomes are typed. Use a sandboxes_list `id`, or "session"/"default" for home.',
|
|
1060
1070
|
inputSchema: { target: z4.string().min(1) },
|
|
1061
1071
|
},
|
|
1062
1072
|
async ({ target }) => json(await swapActiveSandbox(services, await fleetContext(), target)),
|
|
@@ -1066,7 +1076,7 @@ function registerFleetTools(
|
|
|
1066
1076
|
"sandbox_swap",
|
|
1067
1077
|
{
|
|
1068
1078
|
description:
|
|
1069
|
-
'Swap the active sandbox for this session mid-conversation
|
|
1079
|
+
'Swap the active sandbox for this session mid-conversation. Validates ownership and verified readiness, then advances the route epoch. Same-target swaps also revalidate and fence stale route caches. An operation that encountered provider disappearance is not replayed; retry only after a typed recovery-ready result. Use a sandboxes_list `id`, or "session"/"default" for home.',
|
|
1070
1080
|
inputSchema: { target: z4.string().min(1) },
|
|
1071
1081
|
},
|
|
1072
1082
|
async ({ target }) => json(await swapActiveSandbox(services, await fleetContext(), target)),
|
|
@@ -1401,7 +1411,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1401
1411
|
},
|
|
1402
1412
|
authorization?.relatedSessionAccess ?? "root",
|
|
1403
1413
|
);
|
|
1404
|
-
return json(
|
|
1414
|
+
return json(
|
|
1415
|
+
boundSessionDetailMcp(
|
|
1416
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, {
|
|
1417
|
+
...projected,
|
|
1418
|
+
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
|
|
1419
|
+
}),
|
|
1420
|
+
),
|
|
1421
|
+
);
|
|
1405
1422
|
},
|
|
1406
1423
|
);
|
|
1407
1424
|
|
|
@@ -1409,7 +1426,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1409
1426
|
"session_events",
|
|
1410
1427
|
{
|
|
1411
1428
|
description:
|
|
1412
|
-
"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 newest
|
|
1429
|
+
"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.",
|
|
1413
1430
|
inputSchema: {
|
|
1414
1431
|
sessionId: z4.string().uuid(),
|
|
1415
1432
|
after: z4.number().int().nonnegative().optional(),
|
|
@@ -1418,6 +1435,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1418
1435
|
direction: z4.enum(SessionEventReadDirection.options).optional(),
|
|
1419
1436
|
mode: z4.enum(SessionEventReadMode.options).optional(),
|
|
1420
1437
|
payloadMode: z4.enum(SessionEventPayloadMode.options).optional(),
|
|
1438
|
+
resultMode: z4.enum(SessionEventResultMode.options).optional(),
|
|
1421
1439
|
includeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1422
1440
|
excludeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1423
1441
|
includeClasses: z4
|
|
@@ -1428,7 +1446,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1428
1446
|
.array(z4.enum(SessionEventSemanticClass.options))
|
|
1429
1447
|
.max(SessionEventSemanticClass.options.length)
|
|
1430
1448
|
.optional(),
|
|
1431
|
-
latest: z4.enum(
|
|
1449
|
+
latest: z4.enum(SessionEventLatestClass.options).optional(),
|
|
1432
1450
|
},
|
|
1433
1451
|
},
|
|
1434
1452
|
async ({
|
|
@@ -1439,6 +1457,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1439
1457
|
direction: requestedDirection,
|
|
1440
1458
|
mode: requestedMode,
|
|
1441
1459
|
payloadMode: requestedPayloadMode,
|
|
1460
|
+
resultMode: requestedResultMode,
|
|
1442
1461
|
includeTypes,
|
|
1443
1462
|
excludeTypes,
|
|
1444
1463
|
includeClasses,
|
|
@@ -1446,6 +1465,11 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1446
1465
|
latest,
|
|
1447
1466
|
}) => {
|
|
1448
1467
|
await authorizeFirstPartySession(deps, grant, sessionId, "session.events.read");
|
|
1468
|
+
const latestClass =
|
|
1469
|
+
latest === undefined ? undefined : sessionEventLatestClassToSemanticClass(latest);
|
|
1470
|
+
if (requestedResultMode === "compact" && latestClass === undefined) {
|
|
1471
|
+
throw new Error("resultMode=compact requires latest");
|
|
1472
|
+
}
|
|
1449
1473
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1450
1474
|
if (
|
|
1451
1475
|
latest &&
|
|
@@ -1456,24 +1480,42 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1456
1480
|
throw new Error("latest cannot be combined with event filters");
|
|
1457
1481
|
}
|
|
1458
1482
|
const mode = requestedMode ?? (after !== undefined ? "forensic" : "monitoring");
|
|
1459
|
-
const direction =
|
|
1483
|
+
const direction = latestClass
|
|
1460
1484
|
? "before"
|
|
1461
1485
|
: (requestedDirection ??
|
|
1462
1486
|
(before !== undefined ? "before" : after !== undefined ? "after" : "before"));
|
|
1463
|
-
const payloadMode =
|
|
1487
|
+
const payloadMode =
|
|
1488
|
+
requestedResultMode === "compact"
|
|
1489
|
+
? "full"
|
|
1490
|
+
: (requestedPayloadMode ?? (mode === "monitoring" ? "summary" : "full"));
|
|
1464
1491
|
const dbPage = await listSessionEventPage(deps.db, grant.workspaceId, sessionId, {
|
|
1465
1492
|
after: after ?? 0,
|
|
1466
1493
|
...(before !== undefined ? { before } : {}),
|
|
1467
1494
|
direction,
|
|
1468
|
-
limit:
|
|
1495
|
+
limit: latestClass ? 1 : boundedSessionEventMcpLimit(limit),
|
|
1469
1496
|
payloadMode,
|
|
1470
1497
|
includeTypes: includeTypes ?? [],
|
|
1471
1498
|
excludeTypes: excludeTypes ?? [],
|
|
1472
|
-
includeClasses:
|
|
1499
|
+
includeClasses: latestClass ? [latestClass] : (includeClasses ?? []),
|
|
1473
1500
|
excludeClasses: excludeClasses ?? [],
|
|
1474
1501
|
...(mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {}),
|
|
1502
|
+
...(latestClass ? { authoritativeLatest: true } : {}),
|
|
1475
1503
|
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
1476
1504
|
});
|
|
1505
|
+
if (requestedResultMode === "compact") {
|
|
1506
|
+
const event = dbPage.events[0];
|
|
1507
|
+
return json(
|
|
1508
|
+
event
|
|
1509
|
+
? boundSessionEventCompactResult(
|
|
1510
|
+
compactSessionEventResult(
|
|
1511
|
+
event,
|
|
1512
|
+
latestClass!,
|
|
1513
|
+
dbPage.coveredSequence ?? { first: event.sequence, last: event.sequence },
|
|
1514
|
+
),
|
|
1515
|
+
)
|
|
1516
|
+
: null,
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1477
1519
|
return json(
|
|
1478
1520
|
boundSessionEventMcpPage({
|
|
1479
1521
|
events: dbPage.events,
|
|
@@ -1534,6 +1576,9 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1534
1576
|
// Workspace-scoped CREATE idempotency key: a retried session_create with
|
|
1535
1577
|
// the same key returns the already-spawned worker instead of a duplicate.
|
|
1536
1578
|
idempotencyKey: z4.string().min(1).max(200).optional(),
|
|
1579
|
+
// Per-session/agent descendant policy. Reductions need only create;
|
|
1580
|
+
// increases are authorized server-side with workspace:admin.
|
|
1581
|
+
maxNestedAgentDepth: z4.number().int().nonnegative().optional(),
|
|
1537
1582
|
// First-party MCP token permissions for the spawned session; every
|
|
1538
1583
|
// permission must be held by this grant (validated in the domain).
|
|
1539
1584
|
// A goal requires goals:manage in the resulting set; it is never
|
|
@@ -1578,10 +1623,21 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1578
1623
|
},
|
|
1579
1624
|
},
|
|
1580
1625
|
async (args) => {
|
|
1581
|
-
|
|
1582
|
-
|
|
1626
|
+
try {
|
|
1627
|
+
if (callerSessionId !== null) {
|
|
1628
|
+
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
1629
|
+
}
|
|
1630
|
+
const created = await createSessionForRequest(deps, grant, grant.workspaceId, args);
|
|
1631
|
+
return json(await withMcpEffectivePolicy(deps, grant.workspaceId, created));
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
if (error instanceof SessionSpawnDeniedError) {
|
|
1634
|
+
return {
|
|
1635
|
+
...json(sessionSpawnDenialEnvelope(error)),
|
|
1636
|
+
isError: true,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
throw error;
|
|
1583
1640
|
}
|
|
1584
|
-
return json(await createSessionForRequest(deps, grant, grant.workspaceId, args));
|
|
1585
1641
|
},
|
|
1586
1642
|
);
|
|
1587
1643
|
}
|
|
@@ -2542,3 +2598,15 @@ function parseMcpDate(raw: string, label: string): Date {
|
|
|
2542
2598
|
}
|
|
2543
2599
|
return date;
|
|
2544
2600
|
}
|
|
2601
|
+
|
|
2602
|
+
async function withMcpEffectivePolicy(
|
|
2603
|
+
deps: ApiRouteDeps,
|
|
2604
|
+
workspaceId: string,
|
|
2605
|
+
session: Session,
|
|
2606
|
+
): Promise<Session> {
|
|
2607
|
+
const [workspaceServerIds, workspaceDefaultServerIds] = await Promise.all([
|
|
2608
|
+
workspaceSessionToolPolicyServerIds(deps.db, workspaceId, deps.settings),
|
|
2609
|
+
workspaceSessionToolPolicyDefaultServerIds(deps.db, workspaceId, deps.settings),
|
|
2610
|
+
]);
|
|
2611
|
+
return sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds);
|
|
2612
|
+
}
|
package/src/mcp/session-view.ts
CHANGED
|
@@ -13,11 +13,16 @@ import type {
|
|
|
13
13
|
Rig,
|
|
14
14
|
Session,
|
|
15
15
|
SessionEvent,
|
|
16
|
+
SessionEventCompactResult,
|
|
16
17
|
SessionEventPayloadMode,
|
|
17
18
|
SessionEventReadDirection,
|
|
18
19
|
SessionEventReadMode,
|
|
19
20
|
} from "@opengeni/contracts";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
boundSessionEventPayload,
|
|
23
|
+
measureSessionEventJson,
|
|
24
|
+
sessionEventJsonBytes,
|
|
25
|
+
} from "@opengeni/contracts";
|
|
21
26
|
|
|
22
27
|
export const SESSION_EVENT_MCP_MAX_BYTES = 64 * 1024;
|
|
23
28
|
export const SESSION_EVENT_MCP_FIELD_MAX_CHARS = 4_000;
|
|
@@ -25,6 +30,102 @@ export const DEFAULT_SESSION_DETAIL_CHARS = 6_000;
|
|
|
25
30
|
export const SESSION_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
26
31
|
export const RIG_DETAIL_MCP_MAX_BYTES = 64 * 1024;
|
|
27
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Keep the single-result MCP response below the same pretty-JSON envelope as
|
|
35
|
+
* event pages. The contracts projection bounds each value independently for
|
|
36
|
+
* HTTP/SDK use; this second boundary accounts for the result identity,
|
|
37
|
+
* failure/truncation metadata, and MCP's pretty-printing overhead.
|
|
38
|
+
*/
|
|
39
|
+
export function boundSessionEventCompactResult(
|
|
40
|
+
result: SessionEventCompactResult,
|
|
41
|
+
maxBytes = SESSION_EVENT_MCP_MAX_BYTES,
|
|
42
|
+
): SessionEventCompactResult {
|
|
43
|
+
const envelopeMaxBytes = Math.max(8 * 1024, maxBytes);
|
|
44
|
+
|
|
45
|
+
const project = (budget: number): SessionEventCompactResult => {
|
|
46
|
+
const noValues = budget <= 0;
|
|
47
|
+
const text =
|
|
48
|
+
noValues || result.text === null ? null : clampString(result.text, Math.max(128, budget));
|
|
49
|
+
const boundValue = (value: unknown): unknown =>
|
|
50
|
+
noValues || value === null
|
|
51
|
+
? null
|
|
52
|
+
: boundSessionEventPayload(value, {
|
|
53
|
+
surface: "http_projection",
|
|
54
|
+
maxBytes: Math.max(1_024, budget),
|
|
55
|
+
});
|
|
56
|
+
const output = boundValue(result.output);
|
|
57
|
+
const resultValue = boundValue(result.result);
|
|
58
|
+
const checkpoint = boundValue(result.checkpoint);
|
|
59
|
+
const receipt = boundValue(result.receipt);
|
|
60
|
+
const failure =
|
|
61
|
+
noValues || result.failure === null
|
|
62
|
+
? null
|
|
63
|
+
: {
|
|
64
|
+
error:
|
|
65
|
+
clampString(result.failure.error ?? "", Math.max(128, Math.floor(budget / 3))) ||
|
|
66
|
+
null,
|
|
67
|
+
code:
|
|
68
|
+
clampString(result.failure.code ?? "", Math.max(128, Math.floor(budget / 6))) || null,
|
|
69
|
+
retryable: result.failure.retryable,
|
|
70
|
+
recovery:
|
|
71
|
+
clampString(result.failure.recovery ?? "", Math.max(128, Math.floor(budget / 3))) ||
|
|
72
|
+
null,
|
|
73
|
+
};
|
|
74
|
+
const changed =
|
|
75
|
+
text !== result.text ||
|
|
76
|
+
output !== result.output ||
|
|
77
|
+
resultValue !== result.result ||
|
|
78
|
+
checkpoint !== result.checkpoint ||
|
|
79
|
+
receipt !== result.receipt ||
|
|
80
|
+
JSON.stringify(failure) !== JSON.stringify(result.failure);
|
|
81
|
+
// A compact result can inherit a source-payload boundary without any of
|
|
82
|
+
// its already-bounded slots changing at the MCP boundary. Keep that loss
|
|
83
|
+
// visible on the model-facing result, but do not manufacture a new byte
|
|
84
|
+
// count: the source projection owns the original/delivered accounting.
|
|
85
|
+
const inheritedSourceBoundary = result.truncation.fields.includes("payload");
|
|
86
|
+
const mcpBoundaryRecorded = changed || inheritedSourceBoundary;
|
|
87
|
+
const deliveredBytes = sessionEventJsonBytes({
|
|
88
|
+
text,
|
|
89
|
+
output,
|
|
90
|
+
result: resultValue,
|
|
91
|
+
failure,
|
|
92
|
+
checkpoint,
|
|
93
|
+
receipt,
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
...result,
|
|
97
|
+
text,
|
|
98
|
+
output,
|
|
99
|
+
result: resultValue,
|
|
100
|
+
failure,
|
|
101
|
+
checkpoint,
|
|
102
|
+
receipt,
|
|
103
|
+
truncation: {
|
|
104
|
+
...result.truncation,
|
|
105
|
+
truncated: result.truncation.truncated || changed,
|
|
106
|
+
fields: mcpBoundaryRecorded
|
|
107
|
+
? [...new Set([...result.truncation.fields, "mcp_envelope"])]
|
|
108
|
+
: result.truncation.fields,
|
|
109
|
+
originalBytes: changed
|
|
110
|
+
? (result.truncation.originalBytes ?? result.truncation.deliveredBytes)
|
|
111
|
+
: result.truncation.originalBytes,
|
|
112
|
+
deliveredBytes,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Start with a generous budget and tighten only if the full compact result
|
|
118
|
+
// would exceed MCP's envelope. This preserves as much result-bearing data
|
|
119
|
+
// as possible while guaranteeing a truthful bounded response.
|
|
120
|
+
for (const budget of [12_000, 8_000, 4_000, 2_000, 1_000, 0]) {
|
|
121
|
+
const candidate = project(budget);
|
|
122
|
+
if (prettyJsonBytes(candidate) <= envelopeMaxBytes) return candidate;
|
|
123
|
+
}
|
|
124
|
+
throw new RangeError(
|
|
125
|
+
`Session-event compact result exceeds its ${envelopeMaxBytes}-byte envelope`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
28
129
|
function safeStringify(value: unknown): string {
|
|
29
130
|
if (typeof value === "string") return value;
|
|
30
131
|
try {
|