@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
|
@@ -43,7 +43,7 @@ function profileDirectory(environment: NodeJS.ProcessEnv): string {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
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 =
|
|
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 =
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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;
|