@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.
Files changed (43) hide show
  1. package/README.md +19 -5
  2. package/client/omp-config-surface.tsx +243 -24
  3. package/client/omp-config-views.ts +24 -0
  4. package/client/omp-model-picker-state.ts +145 -0
  5. package/client/omp-model-picker.tsx +282 -0
  6. package/client/omp-routing-editor.tsx +307 -0
  7. package/client/support-diagnostics-state.ts +45 -0
  8. package/index.server.ts +27 -2
  9. package/package.json +2 -8
  10. package/paseo-plugin.json +1 -1
  11. package/server/omp-models.ts +59 -0
  12. package/server/omp-settings.ts +30 -20
  13. package/server/operational-failure-diagnostics.ts +76 -0
  14. package/server/package-version.ts +2 -0
  15. package/server/protocol-violation-diagnostics.ts +169 -0
  16. package/server/provider/catalog.ts +39 -10
  17. package/server/provider/connection.ts +60 -8
  18. package/server/provider/host-tools.ts +284 -34
  19. package/server/provider/mcp-transport.ts +2 -1
  20. package/server/provider/omp-rpc.ts +1011 -107
  21. package/server/provider/profile-providers.ts +7 -2
  22. package/server/provider/registration.ts +12 -2
  23. package/server/provider/security.ts +8 -10
  24. package/server/provider/session-descriptors.ts +45 -11
  25. package/server/provider/session.ts +200 -58
  26. package/server/provider/subsessions.ts +311 -73
  27. package/server/provider/timeline-projector.ts +34 -11
  28. package/server/support-diagnostics.ts +284 -0
  29. package/shared/omp-models.ts +49 -0
  30. package/shared/omp-settings.ts +227 -3
  31. package/shared/support-diagnostics.ts +32 -0
  32. package/CHANGELOG.md +0 -113
  33. package/SUPPORT.md +0 -44
  34. package/TESTING.md +0 -150
  35. package/docs/alpha-release-checklist.md +0 -68
  36. package/docs/configuration.md +0 -126
  37. package/docs/core-provider-issue-audit.md +0 -109
  38. package/docs/images/mcp-authorization-compact.png +0 -0
  39. package/docs/images/mcp-controls-wide.png +0 -0
  40. package/docs/images/plugin-manager.png +0 -0
  41. package/docs/images/workspace-settings.png +0 -0
  42. package/docs/installation.md +0 -89
  43. package/tsconfig.json +0 -16
@@ -43,7 +43,7 @@ function profileDirectory(environment: NodeJS.ProcessEnv): string {
43
43
  );
44
44
  }
45
45
 
46
- /** Paseo 0.8 contribution registration is synchronous; inspect directory names only. */
46
+ /** Contribution registration is synchronous; inspect directory names only. */
47
47
  export function discoverOmpProfilesSync(environment: NodeJS.ProcessEnv = process.env): string[] {
48
48
  let directory: Dir;
49
49
  try {
@@ -151,7 +151,12 @@ export function createProfileOmpProvider(profile: string, options: OmpProviderOp
151
151
  const id = profileProviderId(profile);
152
152
  const environment = fixedEnvironment(profile, options.environment ?? process.env);
153
153
  const sessionDir = resolve(ompSessionDir(environment));
154
- const runtime = options.runtime ?? new OmpRpcRuntime({ environment });
154
+ const runtime =
155
+ options.runtime ??
156
+ new OmpRpcRuntime({
157
+ environment,
158
+ reportProtocolViolation: options.reportProtocolViolation,
159
+ });
155
160
  const readPersistedSessionTranscript = runtime.readPersistedSessionTranscript?.bind(runtime);
156
161
  const assertSessionDir = (requested?: string) => {
157
162
  if (requested !== undefined && resolve(requested) !== sessionDir) {
@@ -3,10 +3,11 @@ import { homedir } from "node:os";
3
3
  import type { ProviderRegistration } from "@getpaseo/plugin/server/provider";
4
4
  import { z } from "zod";
5
5
  import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
6
+ import type { OmpOperationalFailureReporter } from "../operational-failure-diagnostics";
6
7
  import { probeOmpAvailability } from "../provider-diagnostics";
7
8
  import { createOmpConnection, OmpNativeSessionReservations } from "./connection";
8
9
  import type { OmpMcpConnector } from "./host-tools";
9
- import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
10
+ import { type OmpProtocolViolationDiagnostic, OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
10
11
  import { parseOmpProviderOptions } from "./provider-options";
11
12
  import { boundedJsonBytes } from "./security";
12
13
  import { OmpProviderOptionsSchema } from "./settings";
@@ -66,6 +67,8 @@ export interface OmpProviderOptions {
66
67
  mcpInitializationTimeoutMs?: number;
67
68
  mcpConnector?: OmpMcpConnector;
68
69
  browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry;
70
+ reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
71
+ reportOperationalFailure?: OmpOperationalFailureReporter;
69
72
  availabilityProbe?: (
70
73
  options: ProviderCatalogOptionsCompat,
71
74
  timeoutMs: number | undefined,
@@ -85,7 +88,12 @@ function stableJson(value: unknown): string {
85
88
  }
86
89
 
87
90
  export function createOmpProvider(options: OmpProviderOptions = {}): ProviderRegistrationCompat {
88
- const runtime = options.runtime ?? new OmpRpcRuntime({ environment: options.environment });
91
+ const runtime =
92
+ options.runtime ??
93
+ new OmpRpcRuntime({
94
+ environment: options.environment,
95
+ reportProtocolViolation: options.reportProtocolViolation,
96
+ });
89
97
  const nativeReservations = new OmpNativeSessionReservations();
90
98
  return {
91
99
  id: "omp-plugin",
@@ -156,6 +164,8 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
156
164
  options.mcpInitializationTimeoutMs,
157
165
  options.replayTimeoutMs,
158
166
  options.browserAuthorizationRegistry,
167
+ undefined,
168
+ options.reportOperationalFailure,
159
169
  );
160
170
  },
161
171
  };
@@ -83,16 +83,14 @@ function truncateJsonString(value: string, maxBytes: number): string {
83
83
  export function truncateUtf8(value: string, maxBytes: number): string {
84
84
  if (utf8Bytes(value) <= maxBytes) return value;
85
85
  const suffix = "<truncated>";
86
- const budget = Math.max(0, maxBytes - utf8Bytes(suffix));
87
- let output = "";
88
- let bytes = 0;
89
- for (const character of value) {
90
- const characterBytes = utf8Bytes(character);
91
- if (bytes + characterBytes > budget) break;
92
- output += character;
93
- bytes += characterBytes;
94
- }
95
- return `${output}${suffix}`;
86
+ const suffixBytes = utf8Bytes(suffix);
87
+ if (maxBytes <= 0) return "";
88
+ if (maxBytes < suffixBytes) return suffix.slice(0, Math.floor(maxBytes));
89
+
90
+ const bytes = Buffer.from(value, "utf8");
91
+ let end = Math.min(maxBytes - suffixBytes, bytes.byteLength);
92
+ while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
93
+ return `${bytes.subarray(0, end).toString("utf8")}${suffix}`;
96
94
  }
97
95
 
98
96
  export interface BoundedJsonMetrics {
@@ -4,6 +4,7 @@ import { type FileHandle, lstat, open, opendir, realpath } from "node:fs/promise
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
6
6
  import { ompSessionDir } from "../paths";
7
+ import { isValidImagePayload } from "./image";
7
8
 
8
9
  const MAX_DESCRIPTOR_PREFIX_BYTES = 64 * 1024;
9
10
  const MAX_DESCRIPTOR_SUFFIX_BYTES = 64 * 1024;
@@ -48,6 +49,7 @@ export interface OmpPersistedSessionTranscript {
48
49
  nativeSessionId: string;
49
50
  byteLength: number;
50
51
  messages: unknown[];
52
+ imageReplayWarning?: true;
51
53
  }
52
54
  export interface OmpSessionListOptions {
53
55
  cwd?: string;
@@ -66,6 +68,11 @@ interface ScanBudget {
66
68
  exhausted: boolean;
67
69
  }
68
70
 
71
+ interface BlobReplayBudget {
72
+ bytes: number;
73
+ imageReplayWarning?: true;
74
+ }
75
+
69
76
  export function validateNativeSessionId(value: unknown): string {
70
77
  if (typeof value !== "string" || !NATIVE_SESSION_ID.test(value)) {
71
78
  throw new Error("Invalid OMP session identifier");
@@ -156,6 +163,7 @@ async function yieldToEventLoop(): Promise<void> {
156
163
  setImmediate(result.resolve);
157
164
  await result.promise;
158
165
  }
166
+ const UNAVAILABLE_IMAGE_MARKER = "[Image unavailable during session replay]";
159
167
  async function readStableFile(
160
168
  handle: FileHandle,
161
169
  byteLength: number,
@@ -213,6 +221,7 @@ async function hydrateBlobImageData(
213
221
  ) {
214
222
  throw new Error("OMP transcript image blob failed ownership or size validation");
215
223
  }
224
+ budget.bytes += stat.size;
216
225
  const bytes = await readStableFile(
217
226
  handle,
218
227
  stat.size,
@@ -222,7 +231,6 @@ async function hydrateBlobImageData(
222
231
  if (createHash("sha256").update(bytes).digest("hex") !== hash) {
223
232
  throw new Error("OMP transcript image blob failed integrity validation");
224
233
  }
225
- budget.bytes += bytes.byteLength;
226
234
  return bytes.toString("base64");
227
235
  } finally {
228
236
  await handle.close().catch(() => undefined);
@@ -232,11 +240,13 @@ async function hydrateBlobImageData(
232
240
  async function hydrateImageParts(
233
241
  value: unknown,
234
242
  blobDirectory: string,
235
- budget: { bytes: number },
243
+ budget: BlobReplayBudget,
244
+ failureMode: "marker" | "omit",
236
245
  signal?: AbortSignal,
237
246
  ): Promise<unknown> {
238
247
  if (!Array.isArray(value)) return value;
239
248
  let hydrated: unknown[] | undefined;
249
+ let omitted: boolean[] | undefined;
240
250
  for (let index = 0; index < value.length; index += 1) {
241
251
  const part = value[index];
242
252
  if (
@@ -251,30 +261,53 @@ async function hydrateImageParts(
251
261
  ) {
252
262
  continue;
253
263
  }
254
- const data = await hydrateBlobImageData(part.data, blobDirectory, budget, signal);
255
- hydrated ??= [...value];
256
- hydrated[index] = { ...part, data };
264
+ try {
265
+ const data = await hydrateBlobImageData(part.data, blobDirectory, budget, signal);
266
+ const mimeType = "mimeType" in part ? part.mimeType : undefined;
267
+ if (typeof mimeType !== "string" || !isValidImagePayload(data, mimeType, data.length)) {
268
+ throw new Error("OMP transcript image blob failed MIME validation");
269
+ }
270
+ hydrated ??= [...value];
271
+ hydrated[index] = { ...part, data };
272
+ } catch {
273
+ signal?.throwIfAborted();
274
+ budget.imageReplayWarning = true;
275
+ hydrated ??= [...value];
276
+ if (failureMode === "marker") {
277
+ hydrated[index] = { type: "text", text: UNAVAILABLE_IMAGE_MARKER };
278
+ } else {
279
+ omitted ??= [];
280
+ omitted[index] = true;
281
+ }
282
+ }
257
283
  }
258
- return hydrated ?? value;
284
+ if (!hydrated) return value;
285
+ return omitted ? hydrated.filter((_, index) => !omitted[index]) : hydrated;
259
286
  }
260
287
 
261
288
  async function hydratePersistedMessageImages(
262
289
  message: unknown,
263
290
  blobDirectory: string | undefined,
264
- budget: { bytes: number },
291
+ budget: BlobReplayBudget,
265
292
  signal?: AbortSignal,
266
293
  ): Promise<unknown> {
267
294
  if (!blobDirectory || !message || typeof message !== "object" || Array.isArray(message)) {
268
295
  return message;
269
296
  }
270
297
  const record = message as Record<string, unknown>;
271
- let content = await hydrateImageParts(record.content, blobDirectory, budget, signal);
298
+ let content = await hydrateImageParts(record.content, blobDirectory, budget, "marker", signal);
272
299
  if (content && typeof content === "object" && !Array.isArray(content)) {
273
300
  const contentRecord = content as Record<string, unknown>;
274
- const nested = await hydrateImageParts(contentRecord.content, blobDirectory, budget, signal);
301
+ const nested = await hydrateImageParts(
302
+ contentRecord.content,
303
+ blobDirectory,
304
+ budget,
305
+ "marker",
306
+ signal,
307
+ );
275
308
  if (nested !== contentRecord.content) content = { ...contentRecord, content: nested };
276
309
  }
277
- const images = await hydrateImageParts(record.images, blobDirectory, budget, signal);
310
+ const images = await hydrateImageParts(record.images, blobDirectory, budget, "omit", signal);
278
311
  if (content === record.content && images === record.images) return message;
279
312
  return { ...record, content, images };
280
313
  }
@@ -620,7 +653,7 @@ export async function readOmpPersistedSessionTranscript(
620
653
  throw new Error("OMP session transcript exceeds message limits");
621
654
  }
622
655
  const hydratedMessages: unknown[] = [];
623
- const blobBudget = { bytes: 0 };
656
+ const blobBudget: BlobReplayBudget = { bytes: 0 };
624
657
  for (const message of messages) {
625
658
  signal?.throwIfAborted();
626
659
  hydratedMessages.push(
@@ -632,6 +665,7 @@ export async function readOmpPersistedSessionTranscript(
632
665
  nativeSessionId,
633
666
  byteLength: bytes.byteLength,
634
667
  messages: hydratedMessages,
668
+ ...(blobBudget.imageReplayWarning ? { imageReplayWarning: true as const } : {}),
635
669
  };
636
670
  } catch (error) {
637
671
  if (error instanceof Error) throw error;