@omercnet/paseo-omp 0.2.1 → 0.3.0-next.92.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/CHANGELOG.md +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +6 -2
- package/TESTING.md +21 -18
- package/client/composer-pill-settings.tsx +157 -0
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +169 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +8 -3
- package/client/memory-popover.tsx +8 -4
- package/client/omp-config-surface.tsx +189 -29
- package/client/omp-plugin-manager.tsx +302 -131
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +18 -7
- package/client/quota-popover.tsx +8 -3
- package/client/quota-state.ts +16 -7
- package/client/sessions-popover.tsx +8 -3
- package/docs/alpha-release-checklist.md +6 -8
- package/docs/configuration.md +8 -4
- package/docs/core-provider-issue-audit.md +3 -2
- 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 +35 -19
- package/index.client.tsx +339 -123
- package/index.server.ts +44 -14
- package/package.json +7 -8
- package/paseo-plugin.json +2 -2
- package/scripts/prepare-dependencies.mjs +24 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +2 -2
- package/server/omp-config.ts +16 -7
- package/server/omp-plugins.ts +70 -21
- package/server/omp-settings.ts +232 -24
- package/server/paths.ts +128 -11
- package/server/provider/catalog.ts +3 -4
- package/server/provider/connection.ts +213 -9
- package/server/provider/host-tools.ts +71 -0
- package/server/provider/omp-rpc.ts +82 -15
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/session-descriptors.ts +306 -1
- package/server/provider/session.ts +704 -249
- package/server/provider/subsessions.ts +4 -1
- package/server/provider/timeline-projector.ts +70 -33
- package/server/provider-diagnostics.ts +122 -36
- package/server/quota.ts +3 -2
- package/server/sessions.ts +2 -2
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/external-url.ts +21 -0
- package/shared/hub.ts +3 -3
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +2 -1
- package/shared/omp-config.ts +5 -1
- package/shared/omp-plugins.ts +74 -33
- package/shared/omp-settings.ts +8 -1
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +12 -3
- package/shared/quota.ts +2 -1
- package/shared/sessions.ts +2 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { isAbsolute } from "node:path";
|
|
3
4
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
ProviderInputSchema,
|
|
8
9
|
requireProviderCapabilities,
|
|
9
10
|
} from "@getpaseo/plugin/server/provider";
|
|
11
|
+
import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
|
|
10
12
|
import { discoverOmpCatalog } from "./catalog";
|
|
11
13
|
import { normalizeOmpCatalogOptions } from "./config-normalization";
|
|
12
14
|
import type { OmpMcpConnector } from "./host-tools";
|
|
@@ -39,6 +41,7 @@ const SUPPORTED_CAPABILITIES: Readonly<Record<string, true>> = {
|
|
|
39
41
|
"session.subsession": true,
|
|
40
42
|
"session.revert.conversation": true,
|
|
41
43
|
permission: true,
|
|
44
|
+
"timeline.plugin": true,
|
|
42
45
|
};
|
|
43
46
|
const SUPPORTED_INPUTS: Readonly<Record<string, true>> = {
|
|
44
47
|
catalog: true,
|
|
@@ -264,8 +267,164 @@ function validateInputEnvelope(input: unknown): asserts input is ProviderInput {
|
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
269
|
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
export interface OmpConnectionDiagnostic {
|
|
271
|
+
diagnosticId: string;
|
|
272
|
+
operation: string;
|
|
273
|
+
errorClass: "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" | "Error" | "NonError";
|
|
274
|
+
classification:
|
|
275
|
+
| "rpc-response-limit"
|
|
276
|
+
| "rpc-invalid-response"
|
|
277
|
+
| "rpc-timeout"
|
|
278
|
+
| "rpc-closed"
|
|
279
|
+
| "rpc-input-failed"
|
|
280
|
+
| "rpc-exit"
|
|
281
|
+
| "spawn-not-found"
|
|
282
|
+
| "spawn-not-runnable"
|
|
283
|
+
| "spawn-failed"
|
|
284
|
+
| "system-error"
|
|
285
|
+
| "database-error"
|
|
286
|
+
| "catalog-empty"
|
|
287
|
+
| "unexpected";
|
|
288
|
+
stage?: "spawn" | "rpc" | "storage";
|
|
289
|
+
code?: (typeof SYSTEM_ERROR_CODES)[number] | (typeof DATABASE_ERROR_CODES)[number];
|
|
290
|
+
exitCode?: number;
|
|
291
|
+
signal?: (typeof EXIT_SIGNALS)[number];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Match complete, locally authored messages only. Never log an arbitrary message, error name,
|
|
295
|
+
// stack, cause, request payload, or environment: each can contain credentials or prompt text.
|
|
296
|
+
const KNOWN_FAILURES = new Map<string, Pick<OmpConnectionDiagnostic, "classification" | "stage">>([
|
|
297
|
+
[
|
|
298
|
+
"OMP RPC response exceeded command limits",
|
|
299
|
+
{ classification: "rpc-response-limit", stage: "rpc" },
|
|
300
|
+
],
|
|
301
|
+
["OMP RPC response is invalid", { classification: "rpc-invalid-response", stage: "rpc" }],
|
|
302
|
+
["OMP RPC request timed out", { classification: "rpc-timeout", stage: "rpc" }],
|
|
303
|
+
["OMP RPC process is closed", { classification: "rpc-closed", stage: "rpc" }],
|
|
304
|
+
["OMP RPC process was closed", { classification: "rpc-closed", stage: "rpc" }],
|
|
305
|
+
["OMP RPC output channel closed", { classification: "rpc-closed", stage: "rpc" }],
|
|
306
|
+
["OMP RPC input channel failed", { classification: "rpc-input-failed", stage: "rpc" }],
|
|
307
|
+
["OMP reported no available models", { classification: "catalog-empty", stage: "rpc" }],
|
|
308
|
+
["OMP executable was not found", { classification: "spawn-not-found", stage: "spawn" }],
|
|
309
|
+
["OMP executable is not runnable", { classification: "spawn-not-runnable", stage: "spawn" }],
|
|
310
|
+
["OMP process could not be launched", { classification: "spawn-failed", stage: "spawn" }],
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
// These are diagnostic vocabulary, not patterns: an unrecognized code or signal is never emitted.
|
|
314
|
+
const SYSTEM_ERROR_CODES = [
|
|
315
|
+
"ENOENT",
|
|
316
|
+
"EACCES",
|
|
317
|
+
"EPERM",
|
|
318
|
+
"ENOTDIR",
|
|
319
|
+
"EISDIR",
|
|
320
|
+
"ENOSPC",
|
|
321
|
+
"EMFILE",
|
|
322
|
+
"ENFILE",
|
|
323
|
+
"ETIMEDOUT",
|
|
324
|
+
"ECONNRESET",
|
|
325
|
+
"ECONNREFUSED",
|
|
326
|
+
"EPIPE",
|
|
327
|
+
"EIO",
|
|
328
|
+
] as const;
|
|
329
|
+
const DATABASE_ERROR_CODES = [
|
|
330
|
+
"SQLITE_ERROR",
|
|
331
|
+
"SQLITE_BUSY",
|
|
332
|
+
"SQLITE_LOCKED",
|
|
333
|
+
"SQLITE_CANTOPEN",
|
|
334
|
+
"SQLITE_CORRUPT",
|
|
335
|
+
"SQLITE_NOTADB",
|
|
336
|
+
"SQLITE_READONLY",
|
|
337
|
+
"SQLITE_FULL",
|
|
338
|
+
"SQLITE_IOERR",
|
|
339
|
+
"ERR_SQLITE_ERROR",
|
|
340
|
+
] as const;
|
|
341
|
+
const EXIT_SIGNALS = [
|
|
342
|
+
"SIGABRT",
|
|
343
|
+
"SIGALRM",
|
|
344
|
+
"SIGBUS",
|
|
345
|
+
"SIGCHLD",
|
|
346
|
+
"SIGCONT",
|
|
347
|
+
"SIGFPE",
|
|
348
|
+
"SIGHUP",
|
|
349
|
+
"SIGILL",
|
|
350
|
+
"SIGINT",
|
|
351
|
+
"SIGIO",
|
|
352
|
+
"SIGIOT",
|
|
353
|
+
"SIGKILL",
|
|
354
|
+
"SIGPIPE",
|
|
355
|
+
"SIGPOLL",
|
|
356
|
+
"SIGPROF",
|
|
357
|
+
"SIGPWR",
|
|
358
|
+
"SIGQUIT",
|
|
359
|
+
"SIGSEGV",
|
|
360
|
+
"SIGSTKFLT",
|
|
361
|
+
"SIGSTOP",
|
|
362
|
+
"SIGSYS",
|
|
363
|
+
"SIGTERM",
|
|
364
|
+
"SIGTRAP",
|
|
365
|
+
"SIGTSTP",
|
|
366
|
+
"SIGTTIN",
|
|
367
|
+
"SIGTTOU",
|
|
368
|
+
"SIGUNUSED",
|
|
369
|
+
"SIGURG",
|
|
370
|
+
"SIGUSR1",
|
|
371
|
+
"SIGUSR2",
|
|
372
|
+
"SIGVTALRM",
|
|
373
|
+
"SIGWINCH",
|
|
374
|
+
"SIGXCPU",
|
|
375
|
+
"SIGXFSZ",
|
|
376
|
+
"SIGBREAK",
|
|
377
|
+
"SIGLOST",
|
|
378
|
+
"SIGINFO",
|
|
379
|
+
"unknown",
|
|
380
|
+
] as const;
|
|
381
|
+
|
|
382
|
+
function classifyFailure(
|
|
383
|
+
error: unknown,
|
|
384
|
+
): Omit<OmpConnectionDiagnostic, "diagnosticId" | "operation"> {
|
|
385
|
+
const result: Omit<OmpConnectionDiagnostic, "diagnosticId" | "operation"> = {
|
|
386
|
+
errorClass:
|
|
387
|
+
error instanceof TypeError
|
|
388
|
+
? "TypeError"
|
|
389
|
+
: error instanceof RangeError
|
|
390
|
+
? "RangeError"
|
|
391
|
+
: error instanceof SyntaxError
|
|
392
|
+
? "SyntaxError"
|
|
393
|
+
: error instanceof ReferenceError
|
|
394
|
+
? "ReferenceError"
|
|
395
|
+
: error instanceof Error
|
|
396
|
+
? "Error"
|
|
397
|
+
: "NonError",
|
|
398
|
+
classification: "unexpected",
|
|
399
|
+
};
|
|
400
|
+
const message = error instanceof Error ? error.message : undefined;
|
|
401
|
+
if (typeof message === "string") {
|
|
402
|
+
const known = KNOWN_FAILURES.get(message);
|
|
403
|
+
if (known) return { ...result, ...known };
|
|
404
|
+
const exit = /^OMP RPC process exited \(code (-?(?:0|[1-9]\d{0,9}))\)$/.exec(message);
|
|
405
|
+
if (exit && exit[0] === message) {
|
|
406
|
+
const exitCode = Number(exit[1]);
|
|
407
|
+
if (exitCode >= -2147483648 && exitCode <= 4294967295) {
|
|
408
|
+
return { ...result, classification: "rpc-exit", stage: "rpc", exitCode };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const signal = EXIT_SIGNALS.find(
|
|
412
|
+
(value) => message === `OMP RPC process exited (signal ${value})`,
|
|
413
|
+
);
|
|
414
|
+
if (signal) return { ...result, classification: "rpc-exit", stage: "rpc", signal };
|
|
415
|
+
}
|
|
416
|
+
// Inspect data properties only; error-code getters may execute arbitrary application code.
|
|
417
|
+
const code =
|
|
418
|
+
error && typeof error === "object"
|
|
419
|
+
? Object.getOwnPropertyDescriptor(error, "code")?.value
|
|
420
|
+
: undefined;
|
|
421
|
+
const systemCode = SYSTEM_ERROR_CODES.find((value) => value === code);
|
|
422
|
+
if (systemCode) return { ...result, classification: "system-error", code: systemCode };
|
|
423
|
+
const databaseCode = DATABASE_ERROR_CODES.find((value) => value === code);
|
|
424
|
+
if (databaseCode) {
|
|
425
|
+
return { ...result, classification: "database-error", stage: "storage", code: databaseCode };
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
269
428
|
}
|
|
270
429
|
|
|
271
430
|
type NativeReservation = { owner: symbol; quarantined: boolean };
|
|
@@ -494,7 +653,22 @@ export function createOmpConnection(
|
|
|
494
653
|
mcpConnector?: OmpMcpConnector,
|
|
495
654
|
mcpInitializationTimeoutMs?: number,
|
|
496
655
|
replayTimeoutMs?: number,
|
|
656
|
+
browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry,
|
|
657
|
+
reportDiagnostic: (diagnostic: OmpConnectionDiagnostic) => void = (diagnostic) =>
|
|
658
|
+
console.error("OMP provider failure", diagnostic),
|
|
497
659
|
): ProviderConnection {
|
|
660
|
+
const errorDetails = (error: unknown, fallback: string): { message: string } => {
|
|
661
|
+
if (isOmpPublicError(error)) return { message: error.message };
|
|
662
|
+
// The generated ID is the only correlation value we log. It also travels in the public
|
|
663
|
+
// request failure, avoiding any assumption that a caller-supplied request ID is value-safe.
|
|
664
|
+
const diagnosticId = randomUUID();
|
|
665
|
+
try {
|
|
666
|
+
reportDiagnostic({ diagnosticId, operation: fallback, ...classifyFailure(error) });
|
|
667
|
+
} catch {
|
|
668
|
+
// A diagnostic sink must never prevent the request from settling or its cleanup.
|
|
669
|
+
}
|
|
670
|
+
return { message: `${fallback} (diagnostic ${diagnosticId})` };
|
|
671
|
+
};
|
|
498
672
|
const safeCapabilities = [...new Set(capabilities)].filter(
|
|
499
673
|
(capability) =>
|
|
500
674
|
SUPPORTED_CAPABILITIES[capability] &&
|
|
@@ -504,7 +678,12 @@ export function createOmpConnection(
|
|
|
504
678
|
const listeners = new Set<(event: ProviderEvent) => void>();
|
|
505
679
|
const sessions = new Map<
|
|
506
680
|
string,
|
|
507
|
-
{
|
|
681
|
+
{
|
|
682
|
+
token: symbol;
|
|
683
|
+
session: OmpProviderSession;
|
|
684
|
+
nativeSessionId?: string;
|
|
685
|
+
removeBrowserAuthorization?: () => void;
|
|
686
|
+
}
|
|
508
687
|
>();
|
|
509
688
|
const opening = new Map<
|
|
510
689
|
string,
|
|
@@ -525,6 +704,13 @@ export function createOmpConnection(
|
|
|
525
704
|
if (closed) return;
|
|
526
705
|
for (const listener of listeners) listener(event);
|
|
527
706
|
};
|
|
707
|
+
const deleteSession = (sessionId: string, token: symbol): boolean => {
|
|
708
|
+
const slot = sessions.get(sessionId);
|
|
709
|
+
if (slot?.token !== token) return false;
|
|
710
|
+
sessions.delete(sessionId);
|
|
711
|
+
slot.removeBrowserAuthorization?.();
|
|
712
|
+
return true;
|
|
713
|
+
};
|
|
528
714
|
|
|
529
715
|
const requestFailure = (
|
|
530
716
|
requestId: string,
|
|
@@ -696,7 +882,7 @@ export function createOmpConnection(
|
|
|
696
882
|
);
|
|
697
883
|
};
|
|
698
884
|
const retireRewindSession = () => {
|
|
699
|
-
|
|
885
|
+
deleteSession(input.sessionId, token);
|
|
700
886
|
};
|
|
701
887
|
session = await OmpProviderSession.open(
|
|
702
888
|
input,
|
|
@@ -735,19 +921,36 @@ export function createOmpConnection(
|
|
|
735
921
|
nativeReservations.release(nativeSessionId, token);
|
|
736
922
|
return;
|
|
737
923
|
}
|
|
738
|
-
|
|
924
|
+
const browserAgentId = input.config.env.PASEO_AGENT_ID?.trim() || input.sessionId;
|
|
925
|
+
const browserAuthorization = browserAuthorizationRegistry?.register(
|
|
926
|
+
browserAgentId,
|
|
927
|
+
session.openPaseoBrowser.bind(session),
|
|
928
|
+
);
|
|
929
|
+
session.setBrowserAuthorizationIssuer(browserAuthorization?.issue ?? null);
|
|
930
|
+
const removeBrowserAuthorization = browserAuthorization
|
|
931
|
+
? () => {
|
|
932
|
+
session?.setBrowserAuthorizationIssuer(null);
|
|
933
|
+
browserAuthorization.remove();
|
|
934
|
+
}
|
|
935
|
+
: undefined;
|
|
936
|
+
sessions.set(input.sessionId, {
|
|
937
|
+
token,
|
|
938
|
+
session,
|
|
939
|
+
nativeSessionId,
|
|
940
|
+
...(removeBrowserAuthorization ? { removeBrowserAuthorization } : {}),
|
|
941
|
+
});
|
|
739
942
|
await session.publishOpened(input.requestId);
|
|
740
943
|
if (
|
|
741
944
|
closing ||
|
|
742
945
|
controller.signal.aborted ||
|
|
743
946
|
opening.get(input.sessionId)?.token !== token
|
|
744
947
|
) {
|
|
745
|
-
|
|
948
|
+
deleteSession(input.sessionId, token);
|
|
746
949
|
await session.close();
|
|
747
950
|
nativeReservations.release(nativeSessionId, token);
|
|
748
951
|
}
|
|
749
952
|
} catch (error) {
|
|
750
|
-
|
|
953
|
+
deleteSession(input.sessionId, token);
|
|
751
954
|
let cleanupError: unknown;
|
|
752
955
|
if (session) {
|
|
753
956
|
try {
|
|
@@ -841,11 +1044,11 @@ export function createOmpConnection(
|
|
|
841
1044
|
try {
|
|
842
1045
|
await slot.session.close();
|
|
843
1046
|
} catch (error) {
|
|
844
|
-
|
|
1047
|
+
deleteSession(input.sessionId, slot.token);
|
|
845
1048
|
quarantineFailedCleanup(input.sessionId, slot.token, error, slot.nativeSessionId);
|
|
846
1049
|
throw new OmpPublicError("OMP session close failed");
|
|
847
1050
|
}
|
|
848
|
-
|
|
1051
|
+
deleteSession(input.sessionId, slot.token);
|
|
849
1052
|
nativeReservations.release(slot.nativeSessionId, slot.token);
|
|
850
1053
|
emit({ type: "request.completed", requestId: input.requestId });
|
|
851
1054
|
return;
|
|
@@ -862,6 +1065,7 @@ export function createOmpConnection(
|
|
|
862
1065
|
shutdown.abort(shutdownReason);
|
|
863
1066
|
for (const { controller } of opening.values()) controller.abort(shutdownReason);
|
|
864
1067
|
for (const { session } of sessions.values()) session.beginConnectionShutdown();
|
|
1068
|
+
for (const slot of sessions.values()) slot.removeBrowserAuthorization?.();
|
|
865
1069
|
const failures: unknown[] = [];
|
|
866
1070
|
const operationBatch = [...activeOperations];
|
|
867
1071
|
const operationResults = await Promise.allSettled(operationBatch);
|
|
@@ -42,6 +42,8 @@ const MAX_PENDING_HOST_TOOL_CALLS = 64;
|
|
|
42
42
|
const MAX_PENDING_HOST_TOOL_BYTES = 8 * 1024 * 1024;
|
|
43
43
|
const DEFAULT_INITIALIZATION_TIMEOUT_MS = 20_000;
|
|
44
44
|
const DEFAULT_MCP_CALL_LIFETIME_MS = 5 * 60 * 1000;
|
|
45
|
+
const MAX_DIRECT_BROWSER_CALLS = 4;
|
|
46
|
+
const BROWSER_CALL_TIMEOUT_MS = 20_000;
|
|
45
47
|
|
|
46
48
|
export type OmpMcpTool = ConnectedMcpTool;
|
|
47
49
|
export type OmpMcpToolPage = ConnectedMcpToolPage;
|
|
@@ -331,6 +333,17 @@ async function settleCleanup(promises: readonly Promise<void>[]): Promise<void>
|
|
|
331
333
|
.map((result) => result.reason);
|
|
332
334
|
if (failures.length > 0) throw new AggregateError(failures, "OMP MCP cleanup failed");
|
|
333
335
|
}
|
|
336
|
+
function paseoBrowserFailure(error: unknown): OmpPublicError {
|
|
337
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
338
|
+
if (/browser_no_host/iu.test(detail)) {
|
|
339
|
+
return new OmpPublicError("No Paseo desktop browser host is connected");
|
|
340
|
+
}
|
|
341
|
+
if (/browser_disabled/iu.test(detail)) {
|
|
342
|
+
return new OmpPublicError("Paseo browser tools are disabled on this host");
|
|
343
|
+
}
|
|
344
|
+
return new OmpPublicError("Paseo could not open the MCP authorization browser tab");
|
|
345
|
+
}
|
|
346
|
+
|
|
334
347
|
export class OmpHostToolsBridge {
|
|
335
348
|
private runtime: OmpRuntimeSession | null = null;
|
|
336
349
|
private readonly pending = new Map<string, PendingCall>();
|
|
@@ -338,6 +351,7 @@ export class OmpHostToolsBridge {
|
|
|
338
351
|
private generation = 0;
|
|
339
352
|
private closePromise: Promise<void> | null = null;
|
|
340
353
|
private fatalHandler: ((error: Error) => void) | null = null;
|
|
354
|
+
private activeDirectBrowserCalls = 0;
|
|
341
355
|
|
|
342
356
|
readonly labels: ReadonlyMap<string, string>;
|
|
343
357
|
private constructor(
|
|
@@ -515,6 +529,63 @@ export class OmpHostToolsBridge {
|
|
|
515
529
|
onFatal(handler: (error: Error) => void): void {
|
|
516
530
|
this.fatalHandler = handler;
|
|
517
531
|
}
|
|
532
|
+
async openPaseoBrowser(url: string): Promise<void> {
|
|
533
|
+
let parsed: URL;
|
|
534
|
+
try {
|
|
535
|
+
parsed = new URL(url);
|
|
536
|
+
} catch {
|
|
537
|
+
throw new OmpPublicError("MCP authorization URL is invalid");
|
|
538
|
+
}
|
|
539
|
+
if (
|
|
540
|
+
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
|
|
541
|
+
parsed.username ||
|
|
542
|
+
parsed.password
|
|
543
|
+
) {
|
|
544
|
+
throw new OmpPublicError("MCP authorization URL is not safe to open");
|
|
545
|
+
}
|
|
546
|
+
if (!this.runtime || this.closePromise) {
|
|
547
|
+
throw new OmpPublicError("The OMP session is not ready for browser authorization");
|
|
548
|
+
}
|
|
549
|
+
const target = this.targets.get("browser_new_tab");
|
|
550
|
+
if (!target) {
|
|
551
|
+
throw new OmpPublicError("Paseo browser tools are unavailable in this OMP session");
|
|
552
|
+
}
|
|
553
|
+
if (this.activeDirectBrowserCalls >= MAX_DIRECT_BROWSER_CALLS) {
|
|
554
|
+
throw new OmpPublicError("Too many Paseo browser requests are already running");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
this.activeDirectBrowserCalls += 1;
|
|
558
|
+
const controller = new AbortController();
|
|
559
|
+
const deadline = this.callScheduler.set(
|
|
560
|
+
() => controller.abort(new Error("Paseo browser request timed out")),
|
|
561
|
+
BROWSER_CALL_TIMEOUT_MS,
|
|
562
|
+
);
|
|
563
|
+
try {
|
|
564
|
+
const result = normalizeResult(
|
|
565
|
+
await target.connection.callTool(
|
|
566
|
+
target.toolName,
|
|
567
|
+
{ i: "Opening MCP authorization", url },
|
|
568
|
+
{
|
|
569
|
+
signal: controller.signal,
|
|
570
|
+
maxTotalTimeoutMs: BROWSER_CALL_TIMEOUT_MS,
|
|
571
|
+
onProgress() {},
|
|
572
|
+
},
|
|
573
|
+
),
|
|
574
|
+
);
|
|
575
|
+
if (result.isError) {
|
|
576
|
+
const detail = result.content
|
|
577
|
+
.flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
|
|
578
|
+
.join("\n");
|
|
579
|
+
throw paseoBrowserFailure(new Error(detail));
|
|
580
|
+
}
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (error instanceof OmpPublicError) throw error;
|
|
583
|
+
throw paseoBrowserFailure(error);
|
|
584
|
+
} finally {
|
|
585
|
+
this.callScheduler.clear(deadline);
|
|
586
|
+
this.activeDirectBrowserCalls -= 1;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
518
589
|
|
|
519
590
|
handle(
|
|
520
591
|
event: OmpHostToolCall | { type: "host_tool_cancel"; id: string; targetId: string },
|
|
@@ -2,12 +2,14 @@ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { ompDataDir } from "../paths";
|
|
5
6
|
import { isValidImagePayload } from "./image";
|
|
6
7
|
import { boundedJsonBytes, OmpCleanupFailure, OmpPublicError, utf8Bytes } from "./security";
|
|
7
8
|
import {
|
|
8
9
|
listOmpSessionDescriptors,
|
|
9
10
|
type OmpSessionDescriptor,
|
|
10
11
|
type OmpSessionListOptions,
|
|
12
|
+
readOmpPersistedSessionTranscript,
|
|
11
13
|
readOmpPersistedSubagentTranscript,
|
|
12
14
|
validateNativeSessionId,
|
|
13
15
|
} from "./session-descriptors";
|
|
@@ -54,7 +56,9 @@ const MAX_PENDING_ONE_WAY_WRITES = 256;
|
|
|
54
56
|
const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
|
|
55
57
|
const MAX_LINE_PARTS = 4_096;
|
|
56
58
|
const MAX_ARRAY_ITEMS = 512;
|
|
57
|
-
|
|
59
|
+
// Tool-intensive OMP turns legitimately exceed 64 blocks; transport byte/node budgets remain the
|
|
60
|
+
// primary resource bounds.
|
|
61
|
+
export const OMP_MAX_CONTENT_PARTS = 4_096;
|
|
58
62
|
const MAX_TODOS = 256;
|
|
59
63
|
const MAX_ENV_ENTRIES = 256;
|
|
60
64
|
const MAX_ENV_VALUE_LENGTH = 64 * 1024;
|
|
@@ -63,6 +67,9 @@ const MAX_PATH_LENGTH = 4_096;
|
|
|
63
67
|
const WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
|
|
64
68
|
const MAX_TOKEN_COUNT = Number.MAX_SAFE_INTEGER;
|
|
65
69
|
const MAX_COST_USD = 1_000_000_000;
|
|
70
|
+
const MAX_RPC_ERROR_BYTES = 4_096;
|
|
71
|
+
const MAX_RPC_ERROR_CODE_BYTES = 256;
|
|
72
|
+
const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
|
|
66
73
|
const MAX_CONTEXT_PERCENT = 1_000_000;
|
|
67
74
|
function boundedJsonString(maxBytes: number, minBytes = 0) {
|
|
68
75
|
return z.string().refine((value) => {
|
|
@@ -142,11 +149,11 @@ const OmpContentPartSchema = z
|
|
|
142
149
|
});
|
|
143
150
|
const OmpDisplayContentSchema = z.union([
|
|
144
151
|
TEXT,
|
|
145
|
-
z.array(OmpContentPartSchema).max(
|
|
152
|
+
z.array(OmpContentPartSchema).max(OMP_MAX_CONTENT_PARTS),
|
|
146
153
|
]);
|
|
147
154
|
const OmpImageArraySchema = z
|
|
148
155
|
.array(OmpContentPartSchema)
|
|
149
|
-
.max(
|
|
156
|
+
.max(OMP_MAX_CONTENT_PARTS)
|
|
150
157
|
.superRefine((parts, context) => {
|
|
151
158
|
if (parts.some((part) => part.type !== "image")) {
|
|
152
159
|
context.addIssue({ code: "custom", message: "invalid image collection" });
|
|
@@ -259,7 +266,7 @@ const OmpAssistantMessageEventSchema = z
|
|
|
259
266
|
.number()
|
|
260
267
|
.int()
|
|
261
268
|
.nonnegative()
|
|
262
|
-
.max(
|
|
269
|
+
.max(OMP_MAX_CONTENT_PARTS - 1)
|
|
263
270
|
.optional(),
|
|
264
271
|
delta: TEXT.optional(),
|
|
265
272
|
content: z
|
|
@@ -387,7 +394,8 @@ const OmpResponseFrameSchema = z.object({
|
|
|
387
394
|
id: IDENTIFIER,
|
|
388
395
|
success: z.boolean(),
|
|
389
396
|
data: z.unknown().optional(),
|
|
390
|
-
error: boundedString(
|
|
397
|
+
error: boundedString(MAX_RPC_ERROR_BYTES).optional(),
|
|
398
|
+
code: boundedString(MAX_RPC_ERROR_CODE_BYTES, 1).optional(),
|
|
391
399
|
});
|
|
392
400
|
const OmpChunkFrameSchema = z.object({
|
|
393
401
|
type: z.literal("rpc_chunk"),
|
|
@@ -562,6 +570,7 @@ const OmpToolApprovalResponseSchema = z.union([
|
|
|
562
570
|
]);
|
|
563
571
|
const OmpAgentEndEnvelopeSchema = z.object({
|
|
564
572
|
type: z.literal("agent_end"),
|
|
573
|
+
requestId: IDENTIFIER.optional(),
|
|
565
574
|
messageCount: z.number().int().nonnegative().optional(),
|
|
566
575
|
isTerminal: z.boolean().optional(),
|
|
567
576
|
});
|
|
@@ -582,6 +591,7 @@ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
|
|
|
582
591
|
z.object({ type: z.literal("agent_start") }),
|
|
583
592
|
z.object({
|
|
584
593
|
type: z.literal("agent_end"),
|
|
594
|
+
requestId: IDENTIFIER.optional(),
|
|
585
595
|
messages: z.array(OmpMessageSchema).max(MAX_ARRAY_ITEMS).optional(),
|
|
586
596
|
messageCount: z.number().int().nonnegative().optional(),
|
|
587
597
|
isTerminal: z.boolean().optional(),
|
|
@@ -924,7 +934,7 @@ export function parseOmpHostToolAgentResult(value: unknown): OmpHostToolResult["
|
|
|
924
934
|
}
|
|
925
935
|
export type OmpRpcEvent =
|
|
926
936
|
| z.infer<typeof OmpRuntimeEventSchema>
|
|
927
|
-
| { type: "prompt_error"; id: string; error: string }
|
|
937
|
+
| { type: "prompt_error"; id: string; error: string; code?: string }
|
|
928
938
|
| { type: "process_exit"; error: string };
|
|
929
939
|
export type OmpAgentSessionEvent = z.infer<typeof OmpAgentSessionEventSchema>;
|
|
930
940
|
export type OmpSubagentSnapshot = z.infer<typeof OmpSubagentsResultSchema>["subagents"][number];
|
|
@@ -945,6 +955,12 @@ export interface OmpPersistedSubagentMessages {
|
|
|
945
955
|
byteLength: number;
|
|
946
956
|
messages: OmpMessage[];
|
|
947
957
|
}
|
|
958
|
+
export interface OmpPersistedSessionMessages {
|
|
959
|
+
sessionFile: string;
|
|
960
|
+
nativeSessionId: string;
|
|
961
|
+
byteLength: number;
|
|
962
|
+
messages: OmpMessage[];
|
|
963
|
+
}
|
|
948
964
|
|
|
949
965
|
export interface OmpStartOptions {
|
|
950
966
|
cwd: string;
|
|
@@ -997,6 +1013,7 @@ export interface OmpRuntimeSession {
|
|
|
997
1013
|
message: string,
|
|
998
1014
|
images?: readonly OmpImage[],
|
|
999
1015
|
onAccepted?: () => void,
|
|
1016
|
+
onRequested?: (requestId: string) => void,
|
|
1000
1017
|
): Promise<{ requestId: string; agentInvoked?: boolean }>;
|
|
1001
1018
|
compact(customInstructions?: string): Promise<OmpCompactionResult>;
|
|
1002
1019
|
setAutoCompaction(enabled: boolean): Promise<void>;
|
|
@@ -1022,6 +1039,12 @@ export interface OmpRuntime {
|
|
|
1022
1039
|
readonly supportsPersistence: boolean;
|
|
1023
1040
|
startSession(options: OmpStartOptions): Promise<OmpRuntimeSession>;
|
|
1024
1041
|
listSessions(options: OmpSessionListOptions): Promise<OmpSessionDescriptor[]>;
|
|
1042
|
+
readPersistedSessionTranscript?(options: {
|
|
1043
|
+
sessionFile: string;
|
|
1044
|
+
sessionId: string;
|
|
1045
|
+
cwd: string;
|
|
1046
|
+
signal?: AbortSignal;
|
|
1047
|
+
}): Promise<OmpPersistedSessionMessages>;
|
|
1025
1048
|
readPersistedSubagentTranscript(options: {
|
|
1026
1049
|
parentSessionFile: string;
|
|
1027
1050
|
childTranscriptId: string;
|
|
@@ -2074,7 +2097,11 @@ class OmpRpcProcess {
|
|
|
2074
2097
|
private receiveKnownResponse(value: unknown): boolean {
|
|
2075
2098
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2076
2099
|
const frame = value as Record<string, unknown>;
|
|
2077
|
-
if (
|
|
2100
|
+
if (
|
|
2101
|
+
frame.type !== "response" ||
|
|
2102
|
+
typeof frame.id !== "string" ||
|
|
2103
|
+
(!this.pending.has(frame.id) && !this.acceptedPromptIds.has(frame.id))
|
|
2104
|
+
) {
|
|
2078
2105
|
return false;
|
|
2079
2106
|
}
|
|
2080
2107
|
this.receiveResponse(frame);
|
|
@@ -2088,19 +2115,15 @@ class OmpRpcProcess {
|
|
|
2088
2115
|
if (!response.success) {
|
|
2089
2116
|
if (rawId && knownPending) {
|
|
2090
2117
|
this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
|
|
2091
|
-
} else {
|
|
2118
|
+
} else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
|
|
2092
2119
|
this.recordProtocolViolation();
|
|
2093
2120
|
}
|
|
2094
2121
|
return;
|
|
2095
2122
|
}
|
|
2096
2123
|
const pending = this.pending.get(response.data.id);
|
|
2097
2124
|
if (!pending) {
|
|
2098
|
-
if (!response.data.success
|
|
2099
|
-
this.
|
|
2100
|
-
type: "prompt_error",
|
|
2101
|
-
id: response.data.id,
|
|
2102
|
-
error: "OMP prompt scheduling failed",
|
|
2103
|
-
});
|
|
2125
|
+
if (!response.data.success) {
|
|
2126
|
+
this.emitAcceptedPromptFailure(response.data.id, response.data);
|
|
2104
2127
|
}
|
|
2105
2128
|
return;
|
|
2106
2129
|
}
|
|
@@ -2112,7 +2135,15 @@ class OmpRpcProcess {
|
|
|
2112
2135
|
isBranchHistory || isHistory
|
|
2113
2136
|
? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
|
|
2114
2137
|
: 2 * 1024 * 1024;
|
|
2115
|
-
|
|
2138
|
+
// A model catalog contains up to 256 structured models, so its aggregate
|
|
2139
|
+
// node budget must exceed the small state/command-response budget.
|
|
2140
|
+
const responseNodeLimit = isBranchHistory
|
|
2141
|
+
? 4_096
|
|
2142
|
+
: isHistory
|
|
2143
|
+
? 400_000
|
|
2144
|
+
: pending.command === "get_available_models"
|
|
2145
|
+
? 16_384
|
|
2146
|
+
: 2_048;
|
|
2116
2147
|
if (
|
|
2117
2148
|
boundedJsonBytes(
|
|
2118
2149
|
frame,
|
|
@@ -2148,6 +2179,22 @@ class OmpRpcProcess {
|
|
|
2148
2179
|
}
|
|
2149
2180
|
}
|
|
2150
2181
|
|
|
2182
|
+
private emitAcceptedPromptFailure(id: string, frame: Record<string, unknown>): boolean {
|
|
2183
|
+
if (frame.success !== false || !this.acceptedPromptIds.delete(id)) return false;
|
|
2184
|
+
const error = typeof frame.error === "string" ? frame.error : undefined;
|
|
2185
|
+
const nativeError =
|
|
2186
|
+
error && utf8Bytes(error) <= MAX_RPC_ERROR_BYTES ? error : PROMPT_SCHEDULING_FAILURE;
|
|
2187
|
+
const code = typeof frame.code === "string" ? frame.code : undefined;
|
|
2188
|
+
const nativeCode = code && utf8Bytes(code) <= MAX_RPC_ERROR_CODE_BYTES ? code : undefined;
|
|
2189
|
+
this.emit({
|
|
2190
|
+
type: "prompt_error",
|
|
2191
|
+
id,
|
|
2192
|
+
error: nativeError,
|
|
2193
|
+
...(nativeCode ? { code: nativeCode } : {}),
|
|
2194
|
+
});
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2151
2198
|
private takePending(id: string): PendingRequest | undefined {
|
|
2152
2199
|
const pending = this.pending.get(id);
|
|
2153
2200
|
if (!pending) return undefined;
|
|
@@ -2595,6 +2642,7 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
2595
2642
|
message: string,
|
|
2596
2643
|
images: readonly OmpImage[] = [],
|
|
2597
2644
|
onAccepted?: () => void,
|
|
2645
|
+
onRequested?: (requestId: string) => void,
|
|
2598
2646
|
): Promise<{ requestId: string; agentInvoked?: boolean }> {
|
|
2599
2647
|
const safeMessage = validateBoundedText(message, "prompt", MAX_TEXT_LENGTH);
|
|
2600
2648
|
let acknowledgement: z.infer<typeof OmpPromptAckSchema> | undefined;
|
|
@@ -2606,6 +2654,7 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
2606
2654
|
onAccepted?.();
|
|
2607
2655
|
},
|
|
2608
2656
|
);
|
|
2657
|
+
onRequested?.(request.id);
|
|
2609
2658
|
await request.promise;
|
|
2610
2659
|
return { requestId: request.id, ...acknowledgement };
|
|
2611
2660
|
}
|
|
@@ -2664,6 +2713,24 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
2664
2713
|
listOmpSessionDescriptors(options, this.options.environment ?? process.env),
|
|
2665
2714
|
);
|
|
2666
2715
|
}
|
|
2716
|
+
async readPersistedSessionTranscript(options: {
|
|
2717
|
+
sessionFile: string;
|
|
2718
|
+
sessionId: string;
|
|
2719
|
+
cwd: string;
|
|
2720
|
+
signal?: AbortSignal;
|
|
2721
|
+
}): Promise<OmpPersistedSessionMessages> {
|
|
2722
|
+
const transcript = await readOmpPersistedSessionTranscript(
|
|
2723
|
+
options.sessionFile,
|
|
2724
|
+
options.sessionId,
|
|
2725
|
+
options.cwd,
|
|
2726
|
+
options.signal,
|
|
2727
|
+
join(ompDataDir(this.options.environment ?? process.env), "blobs"),
|
|
2728
|
+
);
|
|
2729
|
+
return {
|
|
2730
|
+
...transcript,
|
|
2731
|
+
messages: z.array(OmpMessageSchema).max(100_000).parse(transcript.messages),
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2667
2734
|
async readPersistedSubagentTranscript(options: {
|
|
2668
2735
|
parentSessionFile: string;
|
|
2669
2736
|
childTranscriptId: string;
|