@opengeni/api-router 0.7.3 → 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.js +1 -1
- package/dist/{chunk-EYYTFA7N.js → chunk-QOYQBYHM.js} +1920 -264
- package/dist/chunk-QOYQBYHM.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 +62 -8
- 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/sessions.ts +155 -10
- package/src/routes/workspaces.ts +36 -2
- package/src/sandbox/viewer.ts +7 -1
- 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 {
|
|
@@ -107,6 +112,9 @@ import {
|
|
|
107
112
|
sendAgentSessionMessage,
|
|
108
113
|
steerAgentSession,
|
|
109
114
|
updateSessionTitle,
|
|
115
|
+
sessionWithEffectiveToolPolicy,
|
|
116
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
117
|
+
workspaceSessionToolPolicyServerIds,
|
|
110
118
|
type AgentSessionCommandContext,
|
|
111
119
|
} from "@opengeni/core";
|
|
112
120
|
import {
|
|
@@ -120,6 +128,7 @@ import {
|
|
|
120
128
|
type RunOnOp,
|
|
121
129
|
} from "@opengeni/core";
|
|
122
130
|
import {
|
|
131
|
+
boundSessionEventCompactResult,
|
|
123
132
|
boundSessionEventMcpPage,
|
|
124
133
|
boundSessionDetailMcp,
|
|
125
134
|
boundRigDetailMcp,
|
|
@@ -1401,7 +1410,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1401
1410
|
},
|
|
1402
1411
|
authorization?.relatedSessionAccess ?? "root",
|
|
1403
1412
|
);
|
|
1404
|
-
return json(
|
|
1413
|
+
return json(
|
|
1414
|
+
boundSessionDetailMcp(
|
|
1415
|
+
await withMcpEffectivePolicy(deps, grant.workspaceId, {
|
|
1416
|
+
...projected,
|
|
1417
|
+
effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
|
|
1418
|
+
}),
|
|
1419
|
+
),
|
|
1420
|
+
);
|
|
1405
1421
|
},
|
|
1406
1422
|
);
|
|
1407
1423
|
|
|
@@ -1409,7 +1425,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1409
1425
|
"session_events",
|
|
1410
1426
|
{
|
|
1411
1427
|
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
|
|
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.",
|
|
1413
1429
|
inputSchema: {
|
|
1414
1430
|
sessionId: z4.string().uuid(),
|
|
1415
1431
|
after: z4.number().int().nonnegative().optional(),
|
|
@@ -1418,6 +1434,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1418
1434
|
direction: z4.enum(SessionEventReadDirection.options).optional(),
|
|
1419
1435
|
mode: z4.enum(SessionEventReadMode.options).optional(),
|
|
1420
1436
|
payloadMode: z4.enum(SessionEventPayloadMode.options).optional(),
|
|
1437
|
+
resultMode: z4.enum(SessionEventResultMode.options).optional(),
|
|
1421
1438
|
includeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1422
1439
|
excludeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1423
1440
|
includeClasses: z4
|
|
@@ -1428,7 +1445,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1428
1445
|
.array(z4.enum(SessionEventSemanticClass.options))
|
|
1429
1446
|
.max(SessionEventSemanticClass.options.length)
|
|
1430
1447
|
.optional(),
|
|
1431
|
-
latest: z4.enum(
|
|
1448
|
+
latest: z4.enum(SessionEventLatestClass.options).optional(),
|
|
1432
1449
|
},
|
|
1433
1450
|
},
|
|
1434
1451
|
async ({
|
|
@@ -1439,6 +1456,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1439
1456
|
direction: requestedDirection,
|
|
1440
1457
|
mode: requestedMode,
|
|
1441
1458
|
payloadMode: requestedPayloadMode,
|
|
1459
|
+
resultMode: requestedResultMode,
|
|
1442
1460
|
includeTypes,
|
|
1443
1461
|
excludeTypes,
|
|
1444
1462
|
includeClasses,
|
|
@@ -1446,6 +1464,11 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1446
1464
|
latest,
|
|
1447
1465
|
}) => {
|
|
1448
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
|
+
}
|
|
1449
1472
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1450
1473
|
if (
|
|
1451
1474
|
latest &&
|
|
@@ -1456,24 +1479,42 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1456
1479
|
throw new Error("latest cannot be combined with event filters");
|
|
1457
1480
|
}
|
|
1458
1481
|
const mode = requestedMode ?? (after !== undefined ? "forensic" : "monitoring");
|
|
1459
|
-
const direction =
|
|
1482
|
+
const direction = latestClass
|
|
1460
1483
|
? "before"
|
|
1461
1484
|
: (requestedDirection ??
|
|
1462
1485
|
(before !== undefined ? "before" : after !== undefined ? "after" : "before"));
|
|
1463
|
-
const payloadMode =
|
|
1486
|
+
const payloadMode =
|
|
1487
|
+
requestedResultMode === "compact"
|
|
1488
|
+
? "full"
|
|
1489
|
+
: (requestedPayloadMode ?? (mode === "monitoring" ? "summary" : "full"));
|
|
1464
1490
|
const dbPage = await listSessionEventPage(deps.db, grant.workspaceId, sessionId, {
|
|
1465
1491
|
after: after ?? 0,
|
|
1466
1492
|
...(before !== undefined ? { before } : {}),
|
|
1467
1493
|
direction,
|
|
1468
|
-
limit:
|
|
1494
|
+
limit: latestClass ? 1 : boundedSessionEventMcpLimit(limit),
|
|
1469
1495
|
payloadMode,
|
|
1470
1496
|
includeTypes: includeTypes ?? [],
|
|
1471
1497
|
excludeTypes: excludeTypes ?? [],
|
|
1472
|
-
includeClasses:
|
|
1498
|
+
includeClasses: latestClass ? [latestClass] : (includeClasses ?? []),
|
|
1473
1499
|
excludeClasses: excludeClasses ?? [],
|
|
1474
1500
|
...(mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {}),
|
|
1501
|
+
...(latestClass ? { authoritativeLatest: true } : {}),
|
|
1475
1502
|
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
1476
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
|
+
}
|
|
1477
1518
|
return json(
|
|
1478
1519
|
boundSessionEventMcpPage({
|
|
1479
1520
|
events: dbPage.events,
|
|
@@ -1581,7 +1622,8 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1581
1622
|
if (callerSessionId !== null) {
|
|
1582
1623
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
1583
1624
|
}
|
|
1584
|
-
|
|
1625
|
+
const created = await createSessionForRequest(deps, grant, grant.workspaceId, args);
|
|
1626
|
+
return json(await withMcpEffectivePolicy(deps, grant.workspaceId, created));
|
|
1585
1627
|
},
|
|
1586
1628
|
);
|
|
1587
1629
|
}
|
|
@@ -2542,3 +2584,15 @@ function parseMcpDate(raw: string, label: string): Date {
|
|
|
2542
2584
|
}
|
|
2543
2585
|
return date;
|
|
2544
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
|
+
}
|
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 {
|