@omercnet/paseo-omp 0.3.0 → 0.4.0-next.114.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/README.md +19 -5
- package/client/omp-config-surface.tsx +243 -24
- package/client/omp-config-views.ts +24 -0
- package/client/omp-model-picker-state.ts +145 -0
- package/client/omp-model-picker.tsx +282 -0
- package/client/omp-routing-editor.tsx +307 -0
- package/client/support-diagnostics-state.ts +45 -0
- package/index.server.ts +27 -2
- package/package.json +2 -8
- package/paseo-plugin.json +1 -1
- package/server/omp-models.ts +59 -0
- package/server/omp-settings.ts +30 -20
- package/server/operational-failure-diagnostics.ts +76 -0
- package/server/package-version.ts +2 -0
- package/server/protocol-violation-diagnostics.ts +169 -0
- package/server/provider/catalog.ts +39 -10
- package/server/provider/connection.ts +60 -8
- package/server/provider/host-tools.ts +284 -34
- package/server/provider/mcp-transport.ts +2 -1
- package/server/provider/omp-rpc.ts +1011 -107
- package/server/provider/profile-providers.ts +7 -2
- package/server/provider/registration.ts +12 -2
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +45 -11
- package/server/provider/session.ts +200 -58
- package/server/provider/subsessions.ts +311 -73
- package/server/provider/timeline-projector.ts +34 -11
- package/server/support-diagnostics.ts +284 -0
- package/shared/omp-models.ts +49 -0
- package/shared/omp-settings.ts +227 -3
- package/shared/support-diagnostics.ts +32 -0
- package/CHANGELOG.md +0 -113
- package/SUPPORT.md +0 -44
- package/TESTING.md +0 -150
- package/docs/alpha-release-checklist.md +0 -68
- package/docs/configuration.md +0 -126
- package/docs/core-provider-issue-audit.md +0 -109
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +0 -89
- package/tsconfig.json +0 -16
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
requireProviderCapabilities,
|
|
10
10
|
} from "@getpaseo/plugin/server/provider";
|
|
11
11
|
import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
|
|
12
|
+
import type {
|
|
13
|
+
OmpOperationalFailure,
|
|
14
|
+
OmpOperationalFailureReporter,
|
|
15
|
+
} from "../operational-failure-diagnostics";
|
|
12
16
|
import { discoverOmpCatalog } from "./catalog";
|
|
13
17
|
import { normalizeOmpCatalogOptions } from "./config-normalization";
|
|
14
18
|
import type { OmpMcpConnector } from "./host-tools";
|
|
@@ -58,6 +62,7 @@ const MAX_CONNECTION_SESSIONS = 32;
|
|
|
58
62
|
const MAX_ACTIVE_OPERATIONS = 128;
|
|
59
63
|
const MAX_PROVIDER_INPUT_BYTES = 2 * 1024 * 1024;
|
|
60
64
|
const MAX_NESTED_OPTION_BYTES = 256 * 1024;
|
|
65
|
+
const MAX_ENV_ENTRIES = 256;
|
|
61
66
|
const MAX_NATIVE_SESSION_RESERVATIONS = 256;
|
|
62
67
|
|
|
63
68
|
function hasOwnEntries(value: unknown): boolean {
|
|
@@ -68,6 +73,19 @@ function hasOwnEntries(value: unknown): boolean {
|
|
|
68
73
|
return false;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
function providerOptionsExceedPreflightLimits(value: unknown): boolean {
|
|
77
|
+
if (
|
|
78
|
+
boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES, MAX_ENV_ENTRIES) === Number.POSITIVE_INFINITY
|
|
79
|
+
) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
83
|
+
const unrelatedOptions = Object.fromEntries(
|
|
84
|
+
Object.entries(value).filter(([key]) => key !== "env" && key !== "inheritEnv"),
|
|
85
|
+
);
|
|
86
|
+
return boundedJsonBytes(unrelatedOptions, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
function preflightProviderInput(input: unknown): void {
|
|
72
90
|
if (!input || typeof input !== "object") throw new OmpPublicError("Invalid provider request");
|
|
73
91
|
const record = input as Record<string, unknown>;
|
|
@@ -82,7 +100,16 @@ function preflightProviderInput(input: unknown): void {
|
|
|
82
100
|
throw new OmpPublicError("Session persistence input is too large");
|
|
83
101
|
}
|
|
84
102
|
const config = record.config as Record<string, unknown> | undefined;
|
|
85
|
-
|
|
103
|
+
if (
|
|
104
|
+
(config?.env !== undefined &&
|
|
105
|
+
boundedJsonBytes(config.env, MAX_NESTED_OPTION_BYTES, MAX_ENV_ENTRIES) ===
|
|
106
|
+
Number.POSITIVE_INFINITY) ||
|
|
107
|
+
(config?.providerOptions !== undefined &&
|
|
108
|
+
providerOptionsExceedPreflightLimits(config.providerOptions))
|
|
109
|
+
) {
|
|
110
|
+
throw new OmpPublicError("Session configuration is too large");
|
|
111
|
+
}
|
|
112
|
+
for (const value of [config?.mcpServers, config?.settings]) {
|
|
86
113
|
if (
|
|
87
114
|
value !== undefined &&
|
|
88
115
|
boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
@@ -109,13 +136,17 @@ function preflightProviderInput(input: unknown): void {
|
|
|
109
136
|
}
|
|
110
137
|
}
|
|
111
138
|
if (record.type === "catalog" || record.type === "sessions") {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
if (
|
|
140
|
+
record.providerOptions !== undefined &&
|
|
141
|
+
providerOptionsExceedPreflightLimits(record.providerOptions)
|
|
142
|
+
) {
|
|
143
|
+
throw new OmpPublicError("Provider configuration is too large");
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
record.settings !== undefined &&
|
|
147
|
+
boundedJsonBytes(record.settings, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
148
|
+
) {
|
|
149
|
+
throw new OmpPublicError("Provider configuration is too large");
|
|
119
150
|
}
|
|
120
151
|
}
|
|
121
152
|
if (record.type === "session.prompt") {
|
|
@@ -656,6 +687,7 @@ export function createOmpConnection(
|
|
|
656
687
|
browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry,
|
|
657
688
|
reportDiagnostic: (diagnostic: OmpConnectionDiagnostic) => void = (diagnostic) =>
|
|
658
689
|
console.error("OMP provider failure", diagnostic),
|
|
690
|
+
reportOperationalFailure: OmpOperationalFailureReporter = () => {},
|
|
659
691
|
): ProviderConnection {
|
|
660
692
|
const errorDetails = (error: unknown, fallback: string): { message: string } => {
|
|
661
693
|
if (isOmpPublicError(error)) return { message: error.message };
|
|
@@ -669,6 +701,13 @@ export function createOmpConnection(
|
|
|
669
701
|
}
|
|
670
702
|
return { message: `${fallback} (diagnostic ${diagnosticId})` };
|
|
671
703
|
};
|
|
704
|
+
const recordOperationalFailure = (failure: OmpOperationalFailure) => {
|
|
705
|
+
try {
|
|
706
|
+
reportOperationalFailure(failure);
|
|
707
|
+
} catch {
|
|
708
|
+
// Diagnostics must never affect provider requests or cleanup.
|
|
709
|
+
}
|
|
710
|
+
};
|
|
672
711
|
const safeCapabilities = [...new Set(capabilities)].filter(
|
|
673
712
|
(capability) =>
|
|
674
713
|
SUPPORTED_CAPABILITIES[capability] &&
|
|
@@ -775,6 +814,9 @@ export function createOmpConnection(
|
|
|
775
814
|
},
|
|
776
815
|
});
|
|
777
816
|
} catch (error) {
|
|
817
|
+
if (!closing) {
|
|
818
|
+
recordOperationalFailure({ category: "session-open", stage: "catalog" });
|
|
819
|
+
}
|
|
778
820
|
if (isOmpCleanupFailure(error)) catalogCleanup = error.cleanup;
|
|
779
821
|
if (!closing) requestFailure(input.requestId, error, "OMP catalog discovery failed");
|
|
780
822
|
}
|
|
@@ -898,6 +940,7 @@ export function createOmpConnection(
|
|
|
898
940
|
environment,
|
|
899
941
|
mcpConnector,
|
|
900
942
|
mcpInitializationTimeoutMs,
|
|
943
|
+
reportOperationalFailure,
|
|
901
944
|
);
|
|
902
945
|
const discoveredNativeSessionId = session.persistenceSessionId;
|
|
903
946
|
if (discoveredNativeSessionId) nativeSessionId = discoveredNativeSessionId;
|
|
@@ -950,6 +993,15 @@ export function createOmpConnection(
|
|
|
950
993
|
nativeReservations.release(nativeSessionId, token);
|
|
951
994
|
}
|
|
952
995
|
} catch (error) {
|
|
996
|
+
const openingCancelled =
|
|
997
|
+
closing || controller.signal.aborted || opening.get(input.sessionId)?.token !== token;
|
|
998
|
+
if (!openingCancelled) {
|
|
999
|
+
recordOperationalFailure(
|
|
1000
|
+
input.history === "replay" || nativeSessionId
|
|
1001
|
+
? { category: "replay-recovery", stage: "persisted-replay" }
|
|
1002
|
+
: { category: "session-open", stage: "startup" },
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
953
1005
|
deleteSession(input.sessionId, token);
|
|
954
1006
|
let cleanupError: unknown;
|
|
955
1007
|
if (session) {
|
|
@@ -3,6 +3,11 @@ import type {
|
|
|
3
3
|
ProviderMcpServerConfig,
|
|
4
4
|
ProviderSessionConfig,
|
|
5
5
|
} from "@getpaseo/plugin/server/provider";
|
|
6
|
+
import type {
|
|
7
|
+
OmpOperationalFailure,
|
|
8
|
+
OmpOperationalFailureReporter,
|
|
9
|
+
} from "../operational-failure-diagnostics";
|
|
10
|
+
import { isValidImagePayload } from "./image";
|
|
6
11
|
import {
|
|
7
12
|
type ConnectedMcpClient,
|
|
8
13
|
type ConnectedMcpTool,
|
|
@@ -25,6 +30,11 @@ import {
|
|
|
25
30
|
utf8Bytes,
|
|
26
31
|
} from "./security";
|
|
27
32
|
|
|
33
|
+
type HostToolFailureStage = Extract<
|
|
34
|
+
OmpOperationalFailure,
|
|
35
|
+
{ category: "tool-projector"; stage: `host-tool-${string}` }
|
|
36
|
+
>["stage"];
|
|
37
|
+
|
|
28
38
|
const INTERNAL_PASEO_MCP_PATH = "/mcp/agents";
|
|
29
39
|
const RESERVED_PASEO_NAMESPACE = "paseo";
|
|
30
40
|
const MAX_MCP_SERVERS = 32;
|
|
@@ -37,7 +47,13 @@ const MAX_HOST_TOOL_DESCRIPTION_BYTES = 64 * 1024;
|
|
|
37
47
|
const MAX_HOST_TOOL_SCHEMA_BYTES = 256 * 1024;
|
|
38
48
|
const MAX_HOST_TOOL_CATALOG_BYTES = 768 * 1024;
|
|
39
49
|
const MAX_HOST_TOOL_RESULT_BYTES = 12 * 1024 * 1024;
|
|
50
|
+
const MAX_HOST_TOOL_CONTENT_BLOCKS = 512;
|
|
51
|
+
const MAX_HOST_TOOL_TEXT_BYTES = 1024 * 1024;
|
|
52
|
+
const MAX_HOST_TOOL_IMAGE_DATA_BYTES = 8 * 1024 * 1024;
|
|
40
53
|
const MAX_STRUCTURED_CONTENT_FALLBACK_BYTES = 1024 * 1024;
|
|
54
|
+
const CONTENT_TRUNCATED_NOTICE = "[MCP result content truncated: exceeded size limits]";
|
|
55
|
+
const DETAILS_OMITTED_NOTICE = "[MCP structured content omitted: exceeded size limits]";
|
|
56
|
+
const RESULT_TRUNCATED_NOTICE = "[MCP content truncated; details omitted]";
|
|
41
57
|
const MAX_PENDING_HOST_TOOL_CALLS = 64;
|
|
42
58
|
const MAX_PENDING_HOST_TOOL_BYTES = 8 * 1024 * 1024;
|
|
43
59
|
const DEFAULT_INITIALIZATION_TIMEOUT_MS = 20_000;
|
|
@@ -72,6 +88,7 @@ export interface OmpHostToolsOpenOptions {
|
|
|
72
88
|
initializationTimeoutMs?: number;
|
|
73
89
|
callTimeoutMs?: number;
|
|
74
90
|
callScheduler?: OmpHostToolScheduler;
|
|
91
|
+
reportOperationalFailure?: OmpOperationalFailureReporter;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
type ClassifiedServer = {
|
|
@@ -272,42 +289,171 @@ async function discoverMcpTools(
|
|
|
272
289
|
throw new OmpPublicError("MCP server tool-list pagination exceeds the supported limit");
|
|
273
290
|
}
|
|
274
291
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
292
|
+
type HostToolContentBlock = OmpHostToolResult["result"]["content"][number];
|
|
293
|
+
|
|
294
|
+
function hostToolContentIsBounded(content: readonly HostToolContentBlock[]): boolean {
|
|
295
|
+
return (
|
|
279
296
|
boundedJsonBytes(
|
|
280
|
-
|
|
297
|
+
content,
|
|
298
|
+
MAX_HOST_TOOL_RESULT_BYTES,
|
|
299
|
+
MAX_HOST_TOOL_CONTENT_BLOCKS,
|
|
300
|
+
MAX_HOST_TOOL_IMAGE_DATA_BYTES,
|
|
301
|
+
4_096,
|
|
302
|
+
) !== Number.POSITIVE_INFINITY
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function noticeBlock(text: string): HostToolContentBlock {
|
|
307
|
+
return { type: "text", text };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function appendBoundedNotice(
|
|
311
|
+
content: readonly HostToolContentBlock[],
|
|
312
|
+
notice: string,
|
|
313
|
+
): HostToolContentBlock[] {
|
|
314
|
+
const trailingNotice = content.at(-1)?.text;
|
|
315
|
+
const contentWasTruncated =
|
|
316
|
+
trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
317
|
+
const prefix = (contentWasTruncated ? content.slice(0, -1) : content).slice(
|
|
318
|
+
0,
|
|
319
|
+
MAX_HOST_TOOL_CONTENT_BLOCKS - 1,
|
|
320
|
+
);
|
|
321
|
+
const markerText =
|
|
322
|
+
notice === DETAILS_OMITTED_NOTICE && contentWasTruncated ? RESULT_TRUNCATED_NOTICE : notice;
|
|
323
|
+
const marker = noticeBlock(markerText);
|
|
324
|
+
while (prefix.length > 0 && !hostToolContentIsBounded([...prefix, marker])) prefix.pop();
|
|
325
|
+
return [...prefix, marker];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function boundedTextPrefix(
|
|
329
|
+
prefix: readonly HostToolContentBlock[],
|
|
330
|
+
block: HostToolContentBlock,
|
|
331
|
+
): HostToolContentBlock | undefined {
|
|
332
|
+
if (block.type !== "text" || typeof block.text !== "string") return;
|
|
333
|
+
const marker = noticeBlock(CONTENT_TRUNCATED_NOTICE);
|
|
334
|
+
let low = 0;
|
|
335
|
+
let high = block.text.length;
|
|
336
|
+
let best = "";
|
|
337
|
+
while (low <= high) {
|
|
338
|
+
const middle = Math.floor((low + high) / 2);
|
|
339
|
+
let text = block.text.slice(0, middle);
|
|
340
|
+
if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
|
|
341
|
+
const candidate = { type: "text", text } satisfies HostToolContentBlock;
|
|
342
|
+
if (hostToolContentIsBounded([...prefix, candidate, marker])) {
|
|
343
|
+
best = text;
|
|
344
|
+
low = middle + 1;
|
|
345
|
+
} else {
|
|
346
|
+
high = middle - 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return best ? { type: "text", text: best } : undefined;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function normalizeContent(content: readonly unknown[]): HostToolContentBlock[] {
|
|
353
|
+
const normalized: HostToolContentBlock[] = [];
|
|
354
|
+
for (const value of content) {
|
|
355
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
356
|
+
throw new Error("MCP tool returned invalid content");
|
|
357
|
+
}
|
|
358
|
+
const block = value as Record<string, unknown>;
|
|
359
|
+
if (typeof block.type !== "string" || !block.type || utf8Bytes(block.type) > 256) {
|
|
360
|
+
throw new Error("MCP tool returned invalid content");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let candidate: HostToolContentBlock;
|
|
364
|
+
let blockWasTruncated = false;
|
|
365
|
+
if (block.type === "text") {
|
|
366
|
+
if (typeof block.text !== "string") throw new Error("MCP tool returned invalid text content");
|
|
367
|
+
const text = truncateUtf8(block.text, MAX_HOST_TOOL_TEXT_BYTES);
|
|
368
|
+
candidate = { ...block, type: "text", text } as HostToolContentBlock;
|
|
369
|
+
blockWasTruncated = text !== block.text;
|
|
370
|
+
} else if (block.type === "image") {
|
|
371
|
+
if (typeof block.data !== "string" || typeof block.mimeType !== "string") {
|
|
372
|
+
throw new Error("MCP tool returned invalid image content");
|
|
373
|
+
}
|
|
374
|
+
if (block.data.length > MAX_HOST_TOOL_IMAGE_DATA_BYTES) {
|
|
375
|
+
return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
376
|
+
}
|
|
377
|
+
if (!isValidImagePayload(block.data, block.mimeType, MAX_HOST_TOOL_IMAGE_DATA_BYTES)) {
|
|
378
|
+
throw new Error("MCP tool returned invalid image content");
|
|
379
|
+
}
|
|
380
|
+
candidate = {
|
|
381
|
+
...block,
|
|
382
|
+
type: "image",
|
|
383
|
+
data: block.data,
|
|
384
|
+
mimeType: block.mimeType,
|
|
385
|
+
} as HostToolContentBlock;
|
|
386
|
+
} else {
|
|
387
|
+
candidate = block as HostToolContentBlock;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (normalized.length >= MAX_HOST_TOOL_CONTENT_BLOCKS) {
|
|
391
|
+
return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
392
|
+
}
|
|
393
|
+
if (!hostToolContentIsBounded([...normalized, candidate])) {
|
|
394
|
+
const prefix = boundedTextPrefix(normalized, candidate);
|
|
395
|
+
return appendBoundedNotice(
|
|
396
|
+
prefix ? [...normalized, prefix] : normalized,
|
|
397
|
+
CONTENT_TRUNCATED_NOTICE,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
normalized.push(candidate);
|
|
401
|
+
if (blockWasTruncated) return appendBoundedNotice(normalized, CONTENT_TRUNCATED_NOTICE);
|
|
402
|
+
}
|
|
403
|
+
return normalized;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function structuredContentIsBounded(value: unknown): boolean {
|
|
407
|
+
return (
|
|
408
|
+
boundedJsonBytes(
|
|
409
|
+
value,
|
|
281
410
|
MAX_HOST_TOOL_RESULT_BYTES,
|
|
282
411
|
1_024,
|
|
283
412
|
MAX_HOST_TOOL_RESULT_BYTES,
|
|
284
413
|
4_096,
|
|
285
|
-
)
|
|
286
|
-
)
|
|
287
|
-
|
|
414
|
+
) !== Number.POSITIVE_INFINITY
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function normalizeResult(result: unknown): OmpHostToolResult["result"] {
|
|
419
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
420
|
+
throw new Error("MCP tool returned an invalid result");
|
|
288
421
|
}
|
|
289
422
|
const record = result as Record<string, unknown>;
|
|
290
423
|
if (Array.isArray(record.content)) {
|
|
291
424
|
const details = record.structuredContent;
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
425
|
+
const detailsAreBounded = details === undefined || structuredContentIsBounded(details);
|
|
426
|
+
let content = normalizeContent(record.content);
|
|
427
|
+
if (content.length === 0 && details !== undefined && detailsAreBounded) {
|
|
428
|
+
content = [
|
|
429
|
+
{
|
|
430
|
+
type: "text",
|
|
431
|
+
text: truncateUtf8(JSON.stringify(details), MAX_STRUCTURED_CONTENT_FALLBACK_BYTES),
|
|
432
|
+
},
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
if (!detailsAreBounded) content = appendBoundedNotice(content, DETAILS_OMITTED_NOTICE);
|
|
436
|
+
|
|
437
|
+
let normalized = parseOmpHostToolAgentResult({
|
|
302
438
|
content,
|
|
303
|
-
...(details !== undefined ? { details } : {}),
|
|
439
|
+
...(details !== undefined && detailsAreBounded ? { details } : {}),
|
|
304
440
|
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
305
441
|
});
|
|
442
|
+
if (!structuredContentIsBounded(normalized)) {
|
|
443
|
+
normalized = parseOmpHostToolAgentResult({
|
|
444
|
+
content: appendBoundedNotice(content, DETAILS_OMITTED_NOTICE),
|
|
445
|
+
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return normalized;
|
|
306
449
|
}
|
|
307
450
|
if (Object.hasOwn(record, "toolResult")) {
|
|
451
|
+
const details = record.toolResult;
|
|
452
|
+
const detailsAreBounded = structuredContentIsBounded(details);
|
|
308
453
|
return parseOmpHostToolAgentResult({
|
|
309
|
-
content: [
|
|
310
|
-
details:
|
|
454
|
+
content: [noticeBlock(detailsAreBounded ? "MCP tool completed" : DETAILS_OMITTED_NOTICE)],
|
|
455
|
+
...(detailsAreBounded ? { details } : {}),
|
|
456
|
+
...(typeof record.isError === "boolean" ? { isError: record.isError } : {}),
|
|
311
457
|
});
|
|
312
458
|
}
|
|
313
459
|
throw new Error("MCP tool returned an unsupported result");
|
|
@@ -360,6 +506,7 @@ export class OmpHostToolsBridge {
|
|
|
360
506
|
private readonly targets: ReadonlyMap<string, ToolTarget>,
|
|
361
507
|
private readonly callTimeoutMs: number,
|
|
362
508
|
private readonly callScheduler: OmpHostToolScheduler,
|
|
509
|
+
private readonly reportOperationalFailure: OmpOperationalFailureReporter,
|
|
363
510
|
) {
|
|
364
511
|
this.labels = new Map(definitions.map(({ name, label }) => [name, label ?? name]));
|
|
365
512
|
}
|
|
@@ -476,6 +623,7 @@ export class OmpHostToolsBridge {
|
|
|
476
623
|
targets,
|
|
477
624
|
callTimeoutMs,
|
|
478
625
|
callScheduler,
|
|
626
|
+
options.reportOperationalFailure ?? (() => {}),
|
|
479
627
|
);
|
|
480
628
|
} catch (error) {
|
|
481
629
|
const cleanupTasks = [
|
|
@@ -604,7 +752,11 @@ export class OmpHostToolsBridge {
|
|
|
604
752
|
if (!runtime) return true;
|
|
605
753
|
const target = this.targets.get(event.toolName);
|
|
606
754
|
if (!target) {
|
|
607
|
-
this.
|
|
755
|
+
this.sendOperationalError(
|
|
756
|
+
"host-tool-unknown",
|
|
757
|
+
runtime,
|
|
758
|
+
errorResult(event.id, "Unknown OMP host tool"),
|
|
759
|
+
);
|
|
608
760
|
return true;
|
|
609
761
|
}
|
|
610
762
|
const retainedBytes = boundedJsonBytes(
|
|
@@ -620,7 +772,11 @@ export class OmpHostToolsBridge {
|
|
|
620
772
|
retainedBytes === Number.POSITIVE_INFINITY ||
|
|
621
773
|
this.pendingBytes + retainedBytes > MAX_PENDING_HOST_TOOL_BYTES
|
|
622
774
|
) {
|
|
623
|
-
this.
|
|
775
|
+
this.sendOperationalError(
|
|
776
|
+
"host-tool-capacity",
|
|
777
|
+
runtime,
|
|
778
|
+
errorResult(event.id, "OMP host tool bridge is at capacity"),
|
|
779
|
+
);
|
|
624
780
|
return true;
|
|
625
781
|
}
|
|
626
782
|
const pending: PendingCall = {
|
|
@@ -658,23 +814,30 @@ export class OmpHostToolsBridge {
|
|
|
658
814
|
})
|
|
659
815
|
.then((result) => {
|
|
660
816
|
if (!this.isCurrent(event.id, pending)) return;
|
|
661
|
-
let terminal: OmpHostToolResult;
|
|
662
817
|
try {
|
|
663
818
|
const normalized = normalizeResult(result);
|
|
664
|
-
terminal = {
|
|
819
|
+
const terminal: OmpHostToolResult = {
|
|
665
820
|
type: "host_tool_result",
|
|
666
821
|
id: event.id,
|
|
667
822
|
result: normalized,
|
|
668
823
|
...(normalized.isError !== undefined ? { isError: normalized.isError } : {}),
|
|
669
824
|
};
|
|
825
|
+
if (normalized.isError) {
|
|
826
|
+
this.sendOperationalError("host-tool-call", runtime, terminal, pending);
|
|
827
|
+
} else this.sendTerminal(runtime, terminal, pending);
|
|
670
828
|
} catch {
|
|
671
|
-
|
|
829
|
+
this.sendOperationalError(
|
|
830
|
+
"host-tool-normalization",
|
|
831
|
+
runtime,
|
|
832
|
+
errorResult(event.id, "MCP host tool execution failed"),
|
|
833
|
+
pending,
|
|
834
|
+
);
|
|
672
835
|
}
|
|
673
|
-
this.sendTerminal(runtime, terminal, pending);
|
|
674
836
|
})
|
|
675
837
|
.catch(() => {
|
|
676
838
|
if (!this.isCurrent(event.id, pending)) return;
|
|
677
|
-
this.
|
|
839
|
+
this.sendOperationalError(
|
|
840
|
+
"host-tool-call",
|
|
678
841
|
runtime,
|
|
679
842
|
errorResult(event.id, "MCP host tool execution failed"),
|
|
680
843
|
pending,
|
|
@@ -704,30 +867,113 @@ export class OmpHostToolsBridge {
|
|
|
704
867
|
result: OmpHostToolResult,
|
|
705
868
|
): OmpHostToolResult {
|
|
706
869
|
const limit = runtime.maxHostToolFrameBytes ?? 1024 * 1024;
|
|
870
|
+
const fits = (candidate: OmpHostToolResult) => {
|
|
871
|
+
try {
|
|
872
|
+
return Buffer.byteLength(`${JSON.stringify(candidate)}\n`) <= limit;
|
|
873
|
+
} catch {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
if (fits(result)) return result;
|
|
878
|
+
|
|
879
|
+
const withoutDetails: OmpHostToolResult = {
|
|
880
|
+
...result,
|
|
881
|
+
result: {
|
|
882
|
+
content: appendBoundedNotice(result.result.content, DETAILS_OMITTED_NOTICE),
|
|
883
|
+
...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
if (result.result.details !== undefined && fits(withoutDetails)) return withoutDetails;
|
|
887
|
+
|
|
888
|
+
const trailingNotice = result.result.content.at(-1)?.text;
|
|
889
|
+
const detailsWereOmitted =
|
|
890
|
+
trailingNotice === DETAILS_OMITTED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
891
|
+
const contentWasTruncated =
|
|
892
|
+
trailingNotice === CONTENT_TRUNCATED_NOTICE || trailingNotice === RESULT_TRUNCATED_NOTICE;
|
|
893
|
+
const marker = noticeBlock(
|
|
894
|
+
result.result.details !== undefined || detailsWereOmitted
|
|
895
|
+
? RESULT_TRUNCATED_NOTICE
|
|
896
|
+
: CONTENT_TRUNCATED_NOTICE,
|
|
897
|
+
);
|
|
898
|
+
const sourceContent =
|
|
899
|
+
detailsWereOmitted || contentWasTruncated
|
|
900
|
+
? result.result.content.slice(0, -1)
|
|
901
|
+
: result.result.content;
|
|
902
|
+
const boundedContent: HostToolContentBlock[] = [];
|
|
903
|
+
const terminalWith = (content: HostToolContentBlock[]): OmpHostToolResult => ({
|
|
904
|
+
...result,
|
|
905
|
+
result: {
|
|
906
|
+
content,
|
|
907
|
+
...(result.result.isError !== undefined ? { isError: result.result.isError } : {}),
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
for (const block of sourceContent) {
|
|
911
|
+
const candidate = terminalWith([...boundedContent, block, marker]);
|
|
912
|
+
if (fits(candidate)) {
|
|
913
|
+
boundedContent.push(block);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
917
|
+
let low = 0;
|
|
918
|
+
let high = block.text.length;
|
|
919
|
+
let best = "";
|
|
920
|
+
while (low <= high) {
|
|
921
|
+
const middle = Math.floor((low + high) / 2);
|
|
922
|
+
let text = block.text.slice(0, middle);
|
|
923
|
+
if (/\p{Surrogate}$/u.test(text)) text = text.slice(0, -1);
|
|
924
|
+
if (fits(terminalWith([...boundedContent, { type: "text", text }, marker]))) {
|
|
925
|
+
best = text;
|
|
926
|
+
low = middle + 1;
|
|
927
|
+
} else {
|
|
928
|
+
high = middle - 1;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (best) boundedContent.push({ type: "text", text: best });
|
|
932
|
+
}
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
const bounded = terminalWith([...boundedContent, marker]);
|
|
936
|
+
return fits(bounded) ? bounded : errorResult(result.id, OMP_HOST_TOOL_FRAME_LIMIT_ERROR);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private recordOperationalFailure(failure: OmpOperationalFailure): void {
|
|
707
940
|
try {
|
|
708
|
-
|
|
941
|
+
this.reportOperationalFailure(failure);
|
|
709
942
|
} catch {
|
|
710
|
-
//
|
|
943
|
+
// Diagnostics must never affect host tool execution or result delivery.
|
|
711
944
|
}
|
|
712
|
-
return errorResult(result.id, OMP_HOST_TOOL_FRAME_LIMIT_ERROR);
|
|
713
945
|
}
|
|
714
946
|
|
|
715
|
-
private
|
|
947
|
+
private sendOperationalError(
|
|
948
|
+
stage: HostToolFailureStage,
|
|
716
949
|
runtime: OmpRuntimeSession,
|
|
717
950
|
result: OmpHostToolResult,
|
|
718
951
|
pending?: PendingCall,
|
|
719
952
|
): void {
|
|
953
|
+
if (this.sendTerminal(runtime, result, pending)) {
|
|
954
|
+
this.recordOperationalFailure({ category: "tool-projector", stage });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private sendTerminal(
|
|
959
|
+
runtime: OmpRuntimeSession,
|
|
960
|
+
result: OmpHostToolResult,
|
|
961
|
+
pending?: PendingCall,
|
|
962
|
+
): boolean {
|
|
720
963
|
const bounded = this.boundedTerminal(runtime, result);
|
|
721
|
-
if (pending && !this.isCurrent(bounded.id, pending)) return;
|
|
964
|
+
if (pending && !this.isCurrent(bounded.id, pending)) return false;
|
|
722
965
|
try {
|
|
723
966
|
runtime.sendHostToolResult(bounded);
|
|
967
|
+
return true;
|
|
724
968
|
} catch (error) {
|
|
725
969
|
this.failRuntime(runtime, error);
|
|
970
|
+
return false;
|
|
726
971
|
}
|
|
727
972
|
}
|
|
728
973
|
|
|
729
974
|
private failRuntime(runtime: OmpRuntimeSession, error: unknown): void {
|
|
730
975
|
if (this.runtime !== runtime) return;
|
|
976
|
+
this.recordOperationalFailure({ category: "tool-projector", stage: "host-tool-delivery" });
|
|
731
977
|
const failure =
|
|
732
978
|
error instanceof Error ? error : new Error("OMP host tool result delivery failed");
|
|
733
979
|
this.detach();
|
|
@@ -748,7 +994,11 @@ export class OmpHostToolsBridge {
|
|
|
748
994
|
if (!this.isCurrent(id, pending)) return;
|
|
749
995
|
this.releasePending(id, pending);
|
|
750
996
|
pending.controller.abort(new Error("OMP MCP host tool call timed out"));
|
|
751
|
-
this.
|
|
997
|
+
this.sendOperationalError(
|
|
998
|
+
"host-tool-timeout",
|
|
999
|
+
pending.runtime,
|
|
1000
|
+
errorResult(id, "OMP MCP host tool call timed out"),
|
|
1001
|
+
);
|
|
752
1002
|
}
|
|
753
1003
|
|
|
754
1004
|
private releasePending(id: string, pending: PendingCall): void {
|
|
@@ -224,6 +224,7 @@ export class SupervisedStdioClientTransport implements Transport {
|
|
|
224
224
|
this.notifyClose();
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
|
+
const treeCleanup = this.startTreeCleanup();
|
|
227
228
|
if (!this.exited) {
|
|
228
229
|
try {
|
|
229
230
|
child.stdin.end();
|
|
@@ -231,7 +232,7 @@ export class SupervisedStdioClientTransport implements Transport {
|
|
|
231
232
|
// Process-tree cleanup remains authoritative when stdin is already closed.
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
|
-
const terminated = await
|
|
235
|
+
const terminated = await treeCleanup;
|
|
235
236
|
const exited =
|
|
236
237
|
this.spawnFailedWithoutProcess ||
|
|
237
238
|
this.exited ||
|