@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +25 -13
  3. package/SUPPORT.md +7 -3
  4. package/TESTING.md +21 -18
  5. package/client/composer-pill-settings.tsx +157 -0
  6. package/client/external-url.ts +15 -0
  7. package/client/mcp-authorization.tsx +169 -0
  8. package/client/mcp-popover.tsx +155 -0
  9. package/client/memory-panel.tsx +8 -3
  10. package/client/memory-popover.tsx +8 -4
  11. package/client/omp-config-surface.tsx +189 -29
  12. package/client/omp-plugin-manager.tsx +302 -131
  13. package/client/omp-store-picker.tsx +89 -0
  14. package/client/omp-store-state.ts +45 -0
  15. package/client/paseo-types.ts +9 -0
  16. package/client/provider-diagnostics-state.ts +18 -7
  17. package/client/quota-popover.tsx +8 -3
  18. package/client/quota-state.ts +16 -7
  19. package/client/sessions-popover.tsx +8 -3
  20. package/docs/alpha-release-checklist.md +6 -8
  21. package/docs/configuration.md +8 -4
  22. package/docs/core-provider-issue-audit.md +3 -2
  23. package/docs/images/mcp-authorization-compact.png +0 -0
  24. package/docs/images/mcp-controls-wide.png +0 -0
  25. package/docs/images/plugin-manager.png +0 -0
  26. package/docs/images/workspace-settings.png +0 -0
  27. package/docs/installation.md +35 -19
  28. package/index.client.tsx +339 -123
  29. package/index.server.ts +44 -14
  30. package/package.json +7 -8
  31. package/paseo-plugin.json +2 -2
  32. package/scripts/prepare-dependencies.mjs +24 -0
  33. package/server/mcp-browser.ts +95 -0
  34. package/server/memory.ts +2 -2
  35. package/server/omp-config.ts +16 -7
  36. package/server/omp-plugins.ts +70 -21
  37. package/server/omp-settings.ts +232 -24
  38. package/server/paths.ts +128 -11
  39. package/server/provider/catalog.ts +3 -4
  40. package/server/provider/connection.ts +248 -17
  41. package/server/provider/host-tools.ts +294 -26
  42. package/server/provider/omp-rpc.ts +499 -72
  43. package/server/provider/profile-providers.ts +249 -0
  44. package/server/provider/registration.ts +11 -0
  45. package/server/provider/security.ts +8 -10
  46. package/server/provider/session-descriptors.ts +340 -1
  47. package/server/provider/session.ts +716 -249
  48. package/server/provider/subsessions.ts +25 -2
  49. package/server/provider/timeline-projector.ts +104 -44
  50. package/server/provider-diagnostics.ts +122 -36
  51. package/server/quota.ts +3 -2
  52. package/server/sessions.ts +2 -2
  53. package/shared/composer-pill-settings.ts +28 -0
  54. package/shared/external-url.ts +21 -0
  55. package/shared/hub.ts +3 -3
  56. package/shared/mcp.ts +47 -0
  57. package/shared/memory.ts +2 -1
  58. package/shared/omp-config.ts +5 -1
  59. package/shared/omp-plugins.ts +74 -33
  60. package/shared/omp-settings.ts +8 -1
  61. package/shared/omp-store.ts +58 -0
  62. package/shared/provider-diagnostics.ts +12 -3
  63. package/shared/quota.ts +2 -1
  64. 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,
@@ -55,6 +58,7 @@ const MAX_CONNECTION_SESSIONS = 32;
55
58
  const MAX_ACTIVE_OPERATIONS = 128;
56
59
  const MAX_PROVIDER_INPUT_BYTES = 2 * 1024 * 1024;
57
60
  const MAX_NESTED_OPTION_BYTES = 256 * 1024;
61
+ const MAX_ENV_ENTRIES = 256;
58
62
  const MAX_NATIVE_SESSION_RESERVATIONS = 256;
59
63
 
60
64
  function hasOwnEntries(value: unknown): boolean {
@@ -65,6 +69,19 @@ function hasOwnEntries(value: unknown): boolean {
65
69
  return false;
66
70
  }
67
71
 
72
+ function providerOptionsExceedPreflightLimits(value: unknown): boolean {
73
+ if (
74
+ boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES, MAX_ENV_ENTRIES) === Number.POSITIVE_INFINITY
75
+ ) {
76
+ return true;
77
+ }
78
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
79
+ const unrelatedOptions = Object.fromEntries(
80
+ Object.entries(value).filter(([key]) => key !== "env" && key !== "inheritEnv"),
81
+ );
82
+ return boundedJsonBytes(unrelatedOptions, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY;
83
+ }
84
+
68
85
  function preflightProviderInput(input: unknown): void {
69
86
  if (!input || typeof input !== "object") throw new OmpPublicError("Invalid provider request");
70
87
  const record = input as Record<string, unknown>;
@@ -79,7 +96,16 @@ function preflightProviderInput(input: unknown): void {
79
96
  throw new OmpPublicError("Session persistence input is too large");
80
97
  }
81
98
  const config = record.config as Record<string, unknown> | undefined;
82
- for (const value of [config?.mcpServers, config?.providerOptions, config?.settings]) {
99
+ if (
100
+ (config?.env !== undefined &&
101
+ boundedJsonBytes(config.env, MAX_NESTED_OPTION_BYTES, MAX_ENV_ENTRIES) ===
102
+ Number.POSITIVE_INFINITY) ||
103
+ (config?.providerOptions !== undefined &&
104
+ providerOptionsExceedPreflightLimits(config.providerOptions))
105
+ ) {
106
+ throw new OmpPublicError("Session configuration is too large");
107
+ }
108
+ for (const value of [config?.mcpServers, config?.settings]) {
83
109
  if (
84
110
  value !== undefined &&
85
111
  boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
@@ -106,13 +132,17 @@ function preflightProviderInput(input: unknown): void {
106
132
  }
107
133
  }
108
134
  if (record.type === "catalog" || record.type === "sessions") {
109
- for (const value of [record.providerOptions, record.settings]) {
110
- if (
111
- value !== undefined &&
112
- boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
113
- ) {
114
- throw new OmpPublicError("Provider configuration is too large");
115
- }
135
+ if (
136
+ record.providerOptions !== undefined &&
137
+ providerOptionsExceedPreflightLimits(record.providerOptions)
138
+ ) {
139
+ throw new OmpPublicError("Provider configuration is too large");
140
+ }
141
+ if (
142
+ record.settings !== undefined &&
143
+ boundedJsonBytes(record.settings, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
144
+ ) {
145
+ throw new OmpPublicError("Provider configuration is too large");
116
146
  }
117
147
  }
118
148
  if (record.type === "session.prompt") {
@@ -264,8 +294,164 @@ function validateInputEnvelope(input: unknown): asserts input is ProviderInput {
264
294
  }
265
295
  }
266
296
 
267
- function errorDetails(error: unknown, fallback: string): { message: string } {
268
- return { message: isOmpPublicError(error) ? error.message : fallback };
297
+ export interface OmpConnectionDiagnostic {
298
+ diagnosticId: string;
299
+ operation: string;
300
+ errorClass: "TypeError" | "RangeError" | "SyntaxError" | "ReferenceError" | "Error" | "NonError";
301
+ classification:
302
+ | "rpc-response-limit"
303
+ | "rpc-invalid-response"
304
+ | "rpc-timeout"
305
+ | "rpc-closed"
306
+ | "rpc-input-failed"
307
+ | "rpc-exit"
308
+ | "spawn-not-found"
309
+ | "spawn-not-runnable"
310
+ | "spawn-failed"
311
+ | "system-error"
312
+ | "database-error"
313
+ | "catalog-empty"
314
+ | "unexpected";
315
+ stage?: "spawn" | "rpc" | "storage";
316
+ code?: (typeof SYSTEM_ERROR_CODES)[number] | (typeof DATABASE_ERROR_CODES)[number];
317
+ exitCode?: number;
318
+ signal?: (typeof EXIT_SIGNALS)[number];
319
+ }
320
+
321
+ // Match complete, locally authored messages only. Never log an arbitrary message, error name,
322
+ // stack, cause, request payload, or environment: each can contain credentials or prompt text.
323
+ const KNOWN_FAILURES = new Map<string, Pick<OmpConnectionDiagnostic, "classification" | "stage">>([
324
+ [
325
+ "OMP RPC response exceeded command limits",
326
+ { classification: "rpc-response-limit", stage: "rpc" },
327
+ ],
328
+ ["OMP RPC response is invalid", { classification: "rpc-invalid-response", stage: "rpc" }],
329
+ ["OMP RPC request timed out", { classification: "rpc-timeout", stage: "rpc" }],
330
+ ["OMP RPC process is closed", { classification: "rpc-closed", stage: "rpc" }],
331
+ ["OMP RPC process was closed", { classification: "rpc-closed", stage: "rpc" }],
332
+ ["OMP RPC output channel closed", { classification: "rpc-closed", stage: "rpc" }],
333
+ ["OMP RPC input channel failed", { classification: "rpc-input-failed", stage: "rpc" }],
334
+ ["OMP reported no available models", { classification: "catalog-empty", stage: "rpc" }],
335
+ ["OMP executable was not found", { classification: "spawn-not-found", stage: "spawn" }],
336
+ ["OMP executable is not runnable", { classification: "spawn-not-runnable", stage: "spawn" }],
337
+ ["OMP process could not be launched", { classification: "spawn-failed", stage: "spawn" }],
338
+ ]);
339
+
340
+ // These are diagnostic vocabulary, not patterns: an unrecognized code or signal is never emitted.
341
+ const SYSTEM_ERROR_CODES = [
342
+ "ENOENT",
343
+ "EACCES",
344
+ "EPERM",
345
+ "ENOTDIR",
346
+ "EISDIR",
347
+ "ENOSPC",
348
+ "EMFILE",
349
+ "ENFILE",
350
+ "ETIMEDOUT",
351
+ "ECONNRESET",
352
+ "ECONNREFUSED",
353
+ "EPIPE",
354
+ "EIO",
355
+ ] as const;
356
+ const DATABASE_ERROR_CODES = [
357
+ "SQLITE_ERROR",
358
+ "SQLITE_BUSY",
359
+ "SQLITE_LOCKED",
360
+ "SQLITE_CANTOPEN",
361
+ "SQLITE_CORRUPT",
362
+ "SQLITE_NOTADB",
363
+ "SQLITE_READONLY",
364
+ "SQLITE_FULL",
365
+ "SQLITE_IOERR",
366
+ "ERR_SQLITE_ERROR",
367
+ ] as const;
368
+ const EXIT_SIGNALS = [
369
+ "SIGABRT",
370
+ "SIGALRM",
371
+ "SIGBUS",
372
+ "SIGCHLD",
373
+ "SIGCONT",
374
+ "SIGFPE",
375
+ "SIGHUP",
376
+ "SIGILL",
377
+ "SIGINT",
378
+ "SIGIO",
379
+ "SIGIOT",
380
+ "SIGKILL",
381
+ "SIGPIPE",
382
+ "SIGPOLL",
383
+ "SIGPROF",
384
+ "SIGPWR",
385
+ "SIGQUIT",
386
+ "SIGSEGV",
387
+ "SIGSTKFLT",
388
+ "SIGSTOP",
389
+ "SIGSYS",
390
+ "SIGTERM",
391
+ "SIGTRAP",
392
+ "SIGTSTP",
393
+ "SIGTTIN",
394
+ "SIGTTOU",
395
+ "SIGUNUSED",
396
+ "SIGURG",
397
+ "SIGUSR1",
398
+ "SIGUSR2",
399
+ "SIGVTALRM",
400
+ "SIGWINCH",
401
+ "SIGXCPU",
402
+ "SIGXFSZ",
403
+ "SIGBREAK",
404
+ "SIGLOST",
405
+ "SIGINFO",
406
+ "unknown",
407
+ ] as const;
408
+
409
+ function classifyFailure(
410
+ error: unknown,
411
+ ): Omit<OmpConnectionDiagnostic, "diagnosticId" | "operation"> {
412
+ const result: Omit<OmpConnectionDiagnostic, "diagnosticId" | "operation"> = {
413
+ errorClass:
414
+ error instanceof TypeError
415
+ ? "TypeError"
416
+ : error instanceof RangeError
417
+ ? "RangeError"
418
+ : error instanceof SyntaxError
419
+ ? "SyntaxError"
420
+ : error instanceof ReferenceError
421
+ ? "ReferenceError"
422
+ : error instanceof Error
423
+ ? "Error"
424
+ : "NonError",
425
+ classification: "unexpected",
426
+ };
427
+ const message = error instanceof Error ? error.message : undefined;
428
+ if (typeof message === "string") {
429
+ const known = KNOWN_FAILURES.get(message);
430
+ if (known) return { ...result, ...known };
431
+ const exit = /^OMP RPC process exited \(code (-?(?:0|[1-9]\d{0,9}))\)$/.exec(message);
432
+ if (exit && exit[0] === message) {
433
+ const exitCode = Number(exit[1]);
434
+ if (exitCode >= -2147483648 && exitCode <= 4294967295) {
435
+ return { ...result, classification: "rpc-exit", stage: "rpc", exitCode };
436
+ }
437
+ }
438
+ const signal = EXIT_SIGNALS.find(
439
+ (value) => message === `OMP RPC process exited (signal ${value})`,
440
+ );
441
+ if (signal) return { ...result, classification: "rpc-exit", stage: "rpc", signal };
442
+ }
443
+ // Inspect data properties only; error-code getters may execute arbitrary application code.
444
+ const code =
445
+ error && typeof error === "object"
446
+ ? Object.getOwnPropertyDescriptor(error, "code")?.value
447
+ : undefined;
448
+ const systemCode = SYSTEM_ERROR_CODES.find((value) => value === code);
449
+ if (systemCode) return { ...result, classification: "system-error", code: systemCode };
450
+ const databaseCode = DATABASE_ERROR_CODES.find((value) => value === code);
451
+ if (databaseCode) {
452
+ return { ...result, classification: "database-error", stage: "storage", code: databaseCode };
453
+ }
454
+ return result;
269
455
  }
270
456
 
271
457
  type NativeReservation = { owner: symbol; quarantined: boolean };
@@ -494,7 +680,22 @@ export function createOmpConnection(
494
680
  mcpConnector?: OmpMcpConnector,
495
681
  mcpInitializationTimeoutMs?: number,
496
682
  replayTimeoutMs?: number,
683
+ browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry,
684
+ reportDiagnostic: (diagnostic: OmpConnectionDiagnostic) => void = (diagnostic) =>
685
+ console.error("OMP provider failure", diagnostic),
497
686
  ): ProviderConnection {
687
+ const errorDetails = (error: unknown, fallback: string): { message: string } => {
688
+ if (isOmpPublicError(error)) return { message: error.message };
689
+ // The generated ID is the only correlation value we log. It also travels in the public
690
+ // request failure, avoiding any assumption that a caller-supplied request ID is value-safe.
691
+ const diagnosticId = randomUUID();
692
+ try {
693
+ reportDiagnostic({ diagnosticId, operation: fallback, ...classifyFailure(error) });
694
+ } catch {
695
+ // A diagnostic sink must never prevent the request from settling or its cleanup.
696
+ }
697
+ return { message: `${fallback} (diagnostic ${diagnosticId})` };
698
+ };
498
699
  const safeCapabilities = [...new Set(capabilities)].filter(
499
700
  (capability) =>
500
701
  SUPPORTED_CAPABILITIES[capability] &&
@@ -504,7 +705,12 @@ export function createOmpConnection(
504
705
  const listeners = new Set<(event: ProviderEvent) => void>();
505
706
  const sessions = new Map<
506
707
  string,
507
- { token: symbol; session: OmpProviderSession; nativeSessionId?: string }
708
+ {
709
+ token: symbol;
710
+ session: OmpProviderSession;
711
+ nativeSessionId?: string;
712
+ removeBrowserAuthorization?: () => void;
713
+ }
508
714
  >();
509
715
  const opening = new Map<
510
716
  string,
@@ -525,6 +731,13 @@ export function createOmpConnection(
525
731
  if (closed) return;
526
732
  for (const listener of listeners) listener(event);
527
733
  };
734
+ const deleteSession = (sessionId: string, token: symbol): boolean => {
735
+ const slot = sessions.get(sessionId);
736
+ if (slot?.token !== token) return false;
737
+ sessions.delete(sessionId);
738
+ slot.removeBrowserAuthorization?.();
739
+ return true;
740
+ };
528
741
 
529
742
  const requestFailure = (
530
743
  requestId: string,
@@ -696,7 +909,7 @@ export function createOmpConnection(
696
909
  );
697
910
  };
698
911
  const retireRewindSession = () => {
699
- if (sessions.get(input.sessionId)?.token === token) sessions.delete(input.sessionId);
912
+ deleteSession(input.sessionId, token);
700
913
  };
701
914
  session = await OmpProviderSession.open(
702
915
  input,
@@ -735,19 +948,36 @@ export function createOmpConnection(
735
948
  nativeReservations.release(nativeSessionId, token);
736
949
  return;
737
950
  }
738
- sessions.set(input.sessionId, { token, session, nativeSessionId });
951
+ const browserAgentId = input.config.env.PASEO_AGENT_ID?.trim() || input.sessionId;
952
+ const browserAuthorization = browserAuthorizationRegistry?.register(
953
+ browserAgentId,
954
+ session.openPaseoBrowser.bind(session),
955
+ );
956
+ session.setBrowserAuthorizationIssuer(browserAuthorization?.issue ?? null);
957
+ const removeBrowserAuthorization = browserAuthorization
958
+ ? () => {
959
+ session?.setBrowserAuthorizationIssuer(null);
960
+ browserAuthorization.remove();
961
+ }
962
+ : undefined;
963
+ sessions.set(input.sessionId, {
964
+ token,
965
+ session,
966
+ nativeSessionId,
967
+ ...(removeBrowserAuthorization ? { removeBrowserAuthorization } : {}),
968
+ });
739
969
  await session.publishOpened(input.requestId);
740
970
  if (
741
971
  closing ||
742
972
  controller.signal.aborted ||
743
973
  opening.get(input.sessionId)?.token !== token
744
974
  ) {
745
- if (sessions.get(input.sessionId)?.token === token) sessions.delete(input.sessionId);
975
+ deleteSession(input.sessionId, token);
746
976
  await session.close();
747
977
  nativeReservations.release(nativeSessionId, token);
748
978
  }
749
979
  } catch (error) {
750
- if (sessions.get(input.sessionId)?.token === token) sessions.delete(input.sessionId);
980
+ deleteSession(input.sessionId, token);
751
981
  let cleanupError: unknown;
752
982
  if (session) {
753
983
  try {
@@ -841,11 +1071,11 @@ export function createOmpConnection(
841
1071
  try {
842
1072
  await slot.session.close();
843
1073
  } catch (error) {
844
- if (sessions.get(input.sessionId)?.token === slot.token) sessions.delete(input.sessionId);
1074
+ deleteSession(input.sessionId, slot.token);
845
1075
  quarantineFailedCleanup(input.sessionId, slot.token, error, slot.nativeSessionId);
846
1076
  throw new OmpPublicError("OMP session close failed");
847
1077
  }
848
- if (sessions.get(input.sessionId)?.token === slot.token) sessions.delete(input.sessionId);
1078
+ deleteSession(input.sessionId, slot.token);
849
1079
  nativeReservations.release(slot.nativeSessionId, slot.token);
850
1080
  emit({ type: "request.completed", requestId: input.requestId });
851
1081
  return;
@@ -862,6 +1092,7 @@ export function createOmpConnection(
862
1092
  shutdown.abort(shutdownReason);
863
1093
  for (const { controller } of opening.values()) controller.abort(shutdownReason);
864
1094
  for (const { session } of sessions.values()) session.beginConnectionShutdown();
1095
+ for (const slot of sessions.values()) slot.removeBrowserAuthorization?.();
865
1096
  const failures: unknown[] = [];
866
1097
  const operationBatch = [...activeOperations];
867
1098
  const operationResults = await Promise.allSettled(operationBatch);