@omercnet/paseo-omp 0.2.1-next.72.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 +87 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/SUPPORT.md +42 -0
- package/TESTING.md +150 -0
- package/client/composer-pill-settings.tsx +157 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/mcp-authorization.tsx +168 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +76 -0
- package/client/memory-popover.tsx +74 -0
- package/client/omp-config-surface.tsx +1433 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +1004 -0
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/provider-diagnostics-state.ts +262 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +155 -0
- package/client/quota-state.ts +140 -0
- package/client/sessions-popover.tsx +78 -0
- package/docs/alpha-release-checklist.md +68 -0
- package/docs/configuration.md +126 -0
- package/docs/core-provider-issue-audit.md +108 -0
- 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 +67 -0
- package/index.client.tsx +488 -0
- package/index.server.ts +81 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/scripts/prepare-dependencies.mjs +20 -0
- package/server/hub.ts +145 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +135 -0
- package/server/omp-plugins.ts +676 -0
- package/server/omp-settings.ts +499 -0
- package/server/paths.ts +181 -0
- package/server/provider/catalog.ts +172 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +1196 -0
- package/server/provider/host-tools.ts +777 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2806 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +162 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +736 -0
- package/server/provider/session.ts +4796 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +850 -0
- package/server/provider/timeline-projector.ts +1801 -0
- package/server/provider-diagnostics.ts +1143 -0
- package/server/quota.ts +55 -0
- package/server/sessions.ts +58 -0
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/hub.ts +43 -0
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +24 -0
- package/shared/omp-config.ts +85 -0
- package/shared/omp-plugins.ts +264 -0
- package/shared/omp-settings.ts +214 -0
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +126 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +23 -0
- package/shared/sessions.ts +24 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
type ProviderConnection,
|
|
6
|
+
type ProviderEvent,
|
|
7
|
+
type ProviderInput,
|
|
8
|
+
ProviderInputSchema,
|
|
9
|
+
requireProviderCapabilities,
|
|
10
|
+
} from "@getpaseo/plugin/server/provider";
|
|
11
|
+
import type { OmpBrowserAuthorizationRegistry } from "../mcp-browser";
|
|
12
|
+
import { discoverOmpCatalog } from "./catalog";
|
|
13
|
+
import { normalizeOmpCatalogOptions } from "./config-normalization";
|
|
14
|
+
import type { OmpMcpConnector } from "./host-tools";
|
|
15
|
+
import type { OmpRuntime } from "./omp-rpc";
|
|
16
|
+
import {
|
|
17
|
+
boundedJsonBytes,
|
|
18
|
+
isOmpCleanupFailure,
|
|
19
|
+
isOmpPublicError,
|
|
20
|
+
OmpCleanupFailure,
|
|
21
|
+
OmpPublicError,
|
|
22
|
+
utf8Bytes,
|
|
23
|
+
} from "./security";
|
|
24
|
+
import { OmpProviderSession, ompPersistenceSessionId } from "./session";
|
|
25
|
+
|
|
26
|
+
type ProviderConfigurationCompat = {
|
|
27
|
+
providerOptions?: Readonly<Record<string, unknown>>;
|
|
28
|
+
settings?: Readonly<Record<string, unknown>>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
import type { OmpTimelineScheduler } from "./timeline-projector";
|
|
32
|
+
|
|
33
|
+
const SUPPORTED_CAPABILITIES: Readonly<Record<string, true>> = {
|
|
34
|
+
"prompt.message": true,
|
|
35
|
+
"prompt.command": true,
|
|
36
|
+
"prompt.image": true,
|
|
37
|
+
"prompt.steer": true,
|
|
38
|
+
"session.configure": true,
|
|
39
|
+
"session.list": true,
|
|
40
|
+
"session.persistence": true,
|
|
41
|
+
"session.subsession": true,
|
|
42
|
+
"session.revert.conversation": true,
|
|
43
|
+
permission: true,
|
|
44
|
+
"timeline.plugin": true,
|
|
45
|
+
};
|
|
46
|
+
const SUPPORTED_INPUTS: Readonly<Record<string, true>> = {
|
|
47
|
+
catalog: true,
|
|
48
|
+
sessions: true,
|
|
49
|
+
"session.open": true,
|
|
50
|
+
"session.prompt": true,
|
|
51
|
+
"session.configure": true,
|
|
52
|
+
"session.permission": true,
|
|
53
|
+
"session.revert": true,
|
|
54
|
+
"session.interrupt": true,
|
|
55
|
+
"session.close": true,
|
|
56
|
+
};
|
|
57
|
+
const MAX_CONNECTION_SESSIONS = 32;
|
|
58
|
+
const MAX_ACTIVE_OPERATIONS = 128;
|
|
59
|
+
const MAX_PROVIDER_INPUT_BYTES = 2 * 1024 * 1024;
|
|
60
|
+
const MAX_NESTED_OPTION_BYTES = 256 * 1024;
|
|
61
|
+
const MAX_NATIVE_SESSION_RESERVATIONS = 256;
|
|
62
|
+
|
|
63
|
+
function hasOwnEntries(value: unknown): boolean {
|
|
64
|
+
if (!value || typeof value !== "object") return false;
|
|
65
|
+
for (const key in value) {
|
|
66
|
+
if (Object.hasOwn(value, key)) return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function preflightProviderInput(input: unknown): void {
|
|
72
|
+
if (!input || typeof input !== "object") throw new OmpPublicError("Invalid provider request");
|
|
73
|
+
const record = input as Record<string, unknown>;
|
|
74
|
+
if (boundedJsonBytes(record, MAX_PROVIDER_INPUT_BYTES, 512) === Number.POSITIVE_INFINITY) {
|
|
75
|
+
throw new OmpPublicError("Provider request is too large");
|
|
76
|
+
}
|
|
77
|
+
if (record.type === "session.open") {
|
|
78
|
+
if (
|
|
79
|
+
record.persistence !== undefined &&
|
|
80
|
+
boundedJsonBytes(record.persistence, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
81
|
+
) {
|
|
82
|
+
throw new OmpPublicError("Session persistence input is too large");
|
|
83
|
+
}
|
|
84
|
+
const config = record.config as Record<string, unknown> | undefined;
|
|
85
|
+
for (const value of [config?.mcpServers, config?.providerOptions, config?.settings]) {
|
|
86
|
+
if (
|
|
87
|
+
value !== undefined &&
|
|
88
|
+
boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
89
|
+
) {
|
|
90
|
+
throw new OmpPublicError("Session configuration is too large");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (config?.deniedTools !== undefined) {
|
|
94
|
+
if (
|
|
95
|
+
!Array.isArray(config.deniedTools) ||
|
|
96
|
+
config.deniedTools.length > 512 ||
|
|
97
|
+
config.deniedTools.some(
|
|
98
|
+
(tool) => typeof tool !== "string" || tool.trim().length === 0 || utf8Bytes(tool) > 256,
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
throw new OmpPublicError("Invalid denied tool list");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (config?.toolPolicy !== undefined) {
|
|
105
|
+
throw new OmpPublicError("OMP does not support host tool policies");
|
|
106
|
+
}
|
|
107
|
+
if (hasOwnEntries(config?.settings)) {
|
|
108
|
+
throw new OmpPublicError("OMP does not expose live provider settings");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (record.type === "catalog" || record.type === "sessions") {
|
|
112
|
+
for (const value of [record.providerOptions, record.settings]) {
|
|
113
|
+
if (
|
|
114
|
+
value !== undefined &&
|
|
115
|
+
boundedJsonBytes(value, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
116
|
+
) {
|
|
117
|
+
throw new OmpPublicError("Provider configuration is too large");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (record.type === "session.prompt") {
|
|
122
|
+
const prompt = record.prompt as Record<string, unknown> | undefined;
|
|
123
|
+
if (
|
|
124
|
+
prompt?.outputSchema !== undefined &&
|
|
125
|
+
boundedJsonBytes(prompt.outputSchema, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
126
|
+
) {
|
|
127
|
+
throw new OmpPublicError("Prompt output schema is too large");
|
|
128
|
+
}
|
|
129
|
+
if (prompt?.outputSchema !== undefined || prompt?.clearPendingPermissions === true) {
|
|
130
|
+
throw new OmpPublicError("OMP does not support structured output or permission controls");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (record.type === "session.permission") {
|
|
134
|
+
const response = record.response as Record<string, unknown> | undefined;
|
|
135
|
+
if (
|
|
136
|
+
response !== undefined &&
|
|
137
|
+
boundedJsonBytes(response, MAX_NESTED_OPTION_BYTES) === Number.POSITIVE_INFINITY
|
|
138
|
+
) {
|
|
139
|
+
throw new OmpPublicError("Permission response is too large");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function parseProviderInputCompat(input: unknown): ProviderInput {
|
|
144
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
145
|
+
throw new OmpPublicError("Invalid provider request");
|
|
146
|
+
}
|
|
147
|
+
const record = input as Record<string, unknown>;
|
|
148
|
+
if (record.type === "catalog" || record.type === "sessions") {
|
|
149
|
+
const { providerOptions, settings, scope: _scope, force: _force, ...legacyInput } = record;
|
|
150
|
+
const parsed = ProviderInputSchema.safeParse(legacyInput);
|
|
151
|
+
if (!parsed.success) throw new OmpPublicError("Invalid provider request");
|
|
152
|
+
return { ...parsed.data, providerOptions, settings } as unknown as ProviderInput;
|
|
153
|
+
}
|
|
154
|
+
if (record.type === "session.open" && record.config && typeof record.config === "object") {
|
|
155
|
+
const config = record.config as Record<string, unknown>;
|
|
156
|
+
const { deniedTools, ...legacyConfig } = config;
|
|
157
|
+
const parsed = ProviderInputSchema.safeParse({ ...record, config: legacyConfig });
|
|
158
|
+
if (!parsed.success) throw new OmpPublicError("Invalid provider request");
|
|
159
|
+
return {
|
|
160
|
+
...parsed.data,
|
|
161
|
+
config: {
|
|
162
|
+
...(parsed.data as Extract<ProviderInput, { type: "session.open" }>).config,
|
|
163
|
+
deniedTools,
|
|
164
|
+
},
|
|
165
|
+
} as unknown as ProviderInput;
|
|
166
|
+
}
|
|
167
|
+
const parsed = ProviderInputSchema.safeParse(input);
|
|
168
|
+
if (!parsed.success) throw new OmpPublicError("Invalid provider request");
|
|
169
|
+
return parsed.data;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isBoundedIdentifier(value: unknown): value is string {
|
|
173
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validateInputEnvelope(input: unknown): asserts input is ProviderInput {
|
|
177
|
+
if (!input || typeof input !== "object") throw new OmpPublicError("Invalid provider request");
|
|
178
|
+
const record = input as Record<string, unknown>;
|
|
179
|
+
if (typeof record.type !== "string" || !SUPPORTED_INPUTS[record.type]) {
|
|
180
|
+
throw new OmpPublicError("Unsupported provider request");
|
|
181
|
+
}
|
|
182
|
+
if (record.type === "catalog") {
|
|
183
|
+
if (!isBoundedIdentifier(record.requestId))
|
|
184
|
+
throw new OmpPublicError("Invalid provider request");
|
|
185
|
+
if (
|
|
186
|
+
record.cwd !== undefined &&
|
|
187
|
+
(typeof record.cwd !== "string" || utf8Bytes(record.cwd) > 4_096 || record.cwd.includes("\0"))
|
|
188
|
+
) {
|
|
189
|
+
throw new OmpPublicError("Invalid provider request");
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (record.type === "sessions") {
|
|
194
|
+
if (!isBoundedIdentifier(record.requestId))
|
|
195
|
+
throw new OmpPublicError("Invalid provider request");
|
|
196
|
+
if (
|
|
197
|
+
record.cwd !== undefined &&
|
|
198
|
+
(typeof record.cwd !== "string" ||
|
|
199
|
+
record.cwd.length === 0 ||
|
|
200
|
+
!isAbsolute(record.cwd) ||
|
|
201
|
+
utf8Bytes(record.cwd) > 4_096 ||
|
|
202
|
+
record.cwd.includes("\0"))
|
|
203
|
+
) {
|
|
204
|
+
throw new OmpPublicError(
|
|
205
|
+
"OMP session listing requires an absolute working directory when scoped",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (
|
|
209
|
+
record.query !== undefined &&
|
|
210
|
+
(typeof record.query !== "string" || utf8Bytes(record.query) > 512)
|
|
211
|
+
) {
|
|
212
|
+
throw new OmpPublicError("Invalid provider request");
|
|
213
|
+
}
|
|
214
|
+
if (
|
|
215
|
+
record.limit !== undefined &&
|
|
216
|
+
(!Number.isInteger(record.limit) ||
|
|
217
|
+
(record.limit as number) < 1 ||
|
|
218
|
+
(record.limit as number) > 500)
|
|
219
|
+
) {
|
|
220
|
+
throw new OmpPublicError("Invalid provider request");
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!isBoundedIdentifier(record.sessionId)) throw new OmpPublicError("Invalid provider request");
|
|
225
|
+
if (record.type === "session.prompt") {
|
|
226
|
+
if (!record.prompt || typeof record.prompt !== "object") {
|
|
227
|
+
throw new OmpPublicError("Invalid provider request");
|
|
228
|
+
}
|
|
229
|
+
const prompt = record.prompt as Record<string, unknown>;
|
|
230
|
+
if (!isBoundedIdentifier(prompt.clientMessageId)) {
|
|
231
|
+
throw new OmpPublicError("Invalid provider request");
|
|
232
|
+
}
|
|
233
|
+
if (prompt.delivery !== "auto" && prompt.delivery !== "steer") {
|
|
234
|
+
throw new OmpPublicError("Invalid provider request");
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (record.type === "session.permission") {
|
|
239
|
+
if (
|
|
240
|
+
!isBoundedIdentifier(record.permissionId) ||
|
|
241
|
+
!record.response ||
|
|
242
|
+
typeof record.response !== "object"
|
|
243
|
+
) {
|
|
244
|
+
throw new OmpPublicError("Invalid permission response");
|
|
245
|
+
}
|
|
246
|
+
const response = record.response as Record<string, unknown>;
|
|
247
|
+
if (
|
|
248
|
+
response.selectedActionId !== undefined &&
|
|
249
|
+
!isBoundedIdentifier(response.selectedActionId)
|
|
250
|
+
) {
|
|
251
|
+
throw new OmpPublicError("Invalid permission response");
|
|
252
|
+
}
|
|
253
|
+
if (Array.isArray(response.updatedPermissions) && response.updatedPermissions.length > 64) {
|
|
254
|
+
throw new OmpPublicError("Invalid permission response");
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!isBoundedIdentifier(record.requestId)) throw new OmpPublicError("Invalid provider request");
|
|
259
|
+
if (record.type === "session.open" && (!record.config || typeof record.config !== "object")) {
|
|
260
|
+
throw new OmpPublicError("Invalid provider request");
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
record.type === "session.configure" &&
|
|
264
|
+
(!record.changes || typeof record.changes !== "object")
|
|
265
|
+
) {
|
|
266
|
+
throw new OmpPublicError("Invalid provider request");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
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;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
type NativeReservation = { owner: symbol; quarantined: boolean };
|
|
431
|
+
|
|
432
|
+
type NativeReservationRequest =
|
|
433
|
+
| { kind: "list" }
|
|
434
|
+
| { kind: "unknown"; owner: symbol }
|
|
435
|
+
| { kind: "known"; nativeSessionId: string; owner: symbol };
|
|
436
|
+
|
|
437
|
+
type NativeReservationWaiter = NativeReservationRequest & {
|
|
438
|
+
signal?: AbortSignal;
|
|
439
|
+
resolve: () => void;
|
|
440
|
+
reject: (reason?: unknown) => void;
|
|
441
|
+
onAbort?: () => void;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
export class OmpNativeSessionReservations {
|
|
445
|
+
private readonly reservations = new Map<string, NativeReservation>();
|
|
446
|
+
private readonly unknownQuarantines = new Set<symbol>();
|
|
447
|
+
private readonly registrationQueue: NativeReservationWaiter[] = [];
|
|
448
|
+
private openingOwner: symbol | null = null;
|
|
449
|
+
private overflowQuarantines = 0;
|
|
450
|
+
|
|
451
|
+
assertOpenable(): void {
|
|
452
|
+
if (this.hasQuarantine()) {
|
|
453
|
+
throw new OmpPublicError("OMP native session cleanup quarantine is active");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
waitUntilListable(signal?: AbortSignal): Promise<void> {
|
|
458
|
+
return this.enqueue({ kind: "list" }, signal);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
beginPersistentOpen(owner: symbol, signal?: AbortSignal): Promise<void> {
|
|
462
|
+
return this.enqueue({ kind: "unknown", owner }, signal);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
reserve(nativeSessionId: string, owner: symbol, signal?: AbortSignal): Promise<void> {
|
|
466
|
+
return this.enqueue({ kind: "known", nativeSessionId, owner }, signal);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
completePersistentOpen(nativeSessionId: string, owner: symbol): void {
|
|
470
|
+
if (this.openingOwner !== owner) {
|
|
471
|
+
throw new OmpPublicError("OMP persistent session registration was lost");
|
|
472
|
+
}
|
|
473
|
+
const existing = this.reservations.get(nativeSessionId);
|
|
474
|
+
if (existing?.quarantined) {
|
|
475
|
+
throw new OmpPublicError("OMP native session cleanup is unresolved");
|
|
476
|
+
}
|
|
477
|
+
if (existing && existing.owner !== owner) {
|
|
478
|
+
throw new OmpPublicError("OMP native session is already open");
|
|
479
|
+
}
|
|
480
|
+
this.reservations.set(nativeSessionId, { owner, quarantined: false });
|
|
481
|
+
this.openingOwner = null;
|
|
482
|
+
this.drainRegistrationQueue();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
transition(previousSessionId: string, nextSessionId: string, owner: symbol): void {
|
|
486
|
+
if (previousSessionId === nextSessionId) return;
|
|
487
|
+
const previous = this.reservations.get(previousSessionId);
|
|
488
|
+
if (!previous || previous.owner !== owner || previous.quarantined) {
|
|
489
|
+
throw new OmpPublicError("OMP native session ownership changed during rewind");
|
|
490
|
+
}
|
|
491
|
+
const next = this.reservations.get(nextSessionId);
|
|
492
|
+
if (next && next.owner !== owner) {
|
|
493
|
+
throw new OmpPublicError("OMP branched into a native session that is already open");
|
|
494
|
+
}
|
|
495
|
+
this.reservations.set(nextSessionId, { owner, quarantined: false });
|
|
496
|
+
this.reservations.delete(previousSessionId);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
cancelPersistentOpen(owner: symbol): void {
|
|
500
|
+
if (this.openingOwner !== owner) return;
|
|
501
|
+
this.openingOwner = null;
|
|
502
|
+
this.drainRegistrationQueue();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
quarantine(
|
|
506
|
+
nativeSessionId: string | undefined,
|
|
507
|
+
owner: symbol,
|
|
508
|
+
cleanup?: Promise<void>,
|
|
509
|
+
onReleased?: () => void,
|
|
510
|
+
): void {
|
|
511
|
+
if (this.openingOwner === owner) this.openingOwner = null;
|
|
512
|
+
let release: () => boolean;
|
|
513
|
+
const existing = nativeSessionId ? this.reservations.get(nativeSessionId) : undefined;
|
|
514
|
+
if (nativeSessionId && (!existing || existing.owner === owner)) {
|
|
515
|
+
this.reservations.set(nativeSessionId, { owner, quarantined: true });
|
|
516
|
+
release = () => {
|
|
517
|
+
const current = this.reservations.get(nativeSessionId);
|
|
518
|
+
if (!current?.quarantined || current.owner !== owner) return false;
|
|
519
|
+
this.reservations.delete(nativeSessionId);
|
|
520
|
+
return true;
|
|
521
|
+
};
|
|
522
|
+
} else if (this.size < MAX_NATIVE_SESSION_RESERVATIONS) {
|
|
523
|
+
this.unknownQuarantines.add(owner);
|
|
524
|
+
release = () => this.unknownQuarantines.delete(owner);
|
|
525
|
+
} else {
|
|
526
|
+
this.overflowQuarantines += 1;
|
|
527
|
+
release = () => {
|
|
528
|
+
if (this.overflowQuarantines === 0) return false;
|
|
529
|
+
this.overflowQuarantines -= 1;
|
|
530
|
+
return true;
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
this.drainRegistrationQueue();
|
|
534
|
+
if (cleanup) {
|
|
535
|
+
void cleanup.then(
|
|
536
|
+
() => {
|
|
537
|
+
if (release()) onReleased?.();
|
|
538
|
+
},
|
|
539
|
+
() => undefined,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
release(nativeSessionId: string | undefined, owner: symbol): void {
|
|
545
|
+
const reservation = nativeSessionId ? this.reservations.get(nativeSessionId) : undefined;
|
|
546
|
+
if (nativeSessionId && reservation?.owner === owner && !reservation.quarantined) {
|
|
547
|
+
this.reservations.delete(nativeSessionId);
|
|
548
|
+
}
|
|
549
|
+
if (this.openingOwner === owner) this.openingOwner = null;
|
|
550
|
+
this.drainRegistrationQueue();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private enqueue(request: NativeReservationRequest, signal?: AbortSignal): Promise<void> {
|
|
554
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
555
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
556
|
+
const waiter: NativeReservationWaiter = { ...request, signal, resolve, reject };
|
|
557
|
+
if (signal) {
|
|
558
|
+
waiter.onAbort = () => {
|
|
559
|
+
const index = this.registrationQueue.indexOf(waiter);
|
|
560
|
+
if (index < 0) return;
|
|
561
|
+
this.registrationQueue.splice(index, 1);
|
|
562
|
+
this.detachAbort(waiter);
|
|
563
|
+
reject(signal.reason);
|
|
564
|
+
this.drainRegistrationQueue();
|
|
565
|
+
};
|
|
566
|
+
signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
567
|
+
}
|
|
568
|
+
this.registrationQueue.push(waiter);
|
|
569
|
+
this.drainRegistrationQueue();
|
|
570
|
+
return promise;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private drainRegistrationQueue(): void {
|
|
574
|
+
while (!this.openingOwner && this.registrationQueue.length > 0) {
|
|
575
|
+
const waiter = this.registrationQueue.shift();
|
|
576
|
+
if (!waiter) return;
|
|
577
|
+
if (waiter.signal?.aborted) {
|
|
578
|
+
this.rejectWaiter(waiter, waiter.signal.reason);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
this.assertOpenable();
|
|
583
|
+
if (waiter.kind === "list") {
|
|
584
|
+
this.resolveWaiter(waiter);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (waiter.kind === "known") {
|
|
588
|
+
const existing = this.reservations.get(waiter.nativeSessionId);
|
|
589
|
+
if (existing) {
|
|
590
|
+
if (existing.owner !== waiter.owner) {
|
|
591
|
+
throw new OmpPublicError("OMP native session is already open");
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
this.assertCapacity();
|
|
595
|
+
this.reservations.set(waiter.nativeSessionId, {
|
|
596
|
+
owner: waiter.owner,
|
|
597
|
+
quarantined: false,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
this.resolveWaiter(waiter);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
this.assertCapacity();
|
|
604
|
+
this.openingOwner = waiter.owner;
|
|
605
|
+
this.resolveWaiter(waiter);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
this.rejectWaiter(waiter, error);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private resolveWaiter(waiter: NativeReservationWaiter): void {
|
|
613
|
+
this.detachAbort(waiter);
|
|
614
|
+
waiter.resolve();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private rejectWaiter(waiter: NativeReservationWaiter, reason: unknown): void {
|
|
618
|
+
this.detachAbort(waiter);
|
|
619
|
+
waiter.reject(reason);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private detachAbort(waiter: NativeReservationWaiter): void {
|
|
623
|
+
if (waiter.signal && waiter.onAbort) {
|
|
624
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private get size(): number {
|
|
629
|
+
return this.reservations.size + this.unknownQuarantines.size + (this.openingOwner ? 1 : 0);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private hasQuarantine(): boolean {
|
|
633
|
+
if (this.unknownQuarantines.size > 0 || this.overflowQuarantines > 0) return true;
|
|
634
|
+
for (const reservation of this.reservations.values()) {
|
|
635
|
+
if (reservation.quarantined) return true;
|
|
636
|
+
}
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
private assertCapacity(): void {
|
|
641
|
+
if (this.size >= MAX_NATIVE_SESSION_RESERVATIONS) {
|
|
642
|
+
throw new OmpPublicError("OMP persistent session registry limit reached");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export function createOmpConnection(
|
|
648
|
+
runtime: OmpRuntime,
|
|
649
|
+
capabilities: readonly string[],
|
|
650
|
+
scheduler?: OmpTimelineScheduler,
|
|
651
|
+
environment?: NodeJS.ProcessEnv,
|
|
652
|
+
nativeReservations = new OmpNativeSessionReservations(),
|
|
653
|
+
mcpConnector?: OmpMcpConnector,
|
|
654
|
+
mcpInitializationTimeoutMs?: number,
|
|
655
|
+
replayTimeoutMs?: number,
|
|
656
|
+
browserAuthorizationRegistry?: OmpBrowserAuthorizationRegistry,
|
|
657
|
+
reportDiagnostic: (diagnostic: OmpConnectionDiagnostic) => void = (diagnostic) =>
|
|
658
|
+
console.error("OMP provider failure", diagnostic),
|
|
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
|
+
};
|
|
672
|
+
const safeCapabilities = [...new Set(capabilities)].filter(
|
|
673
|
+
(capability) =>
|
|
674
|
+
SUPPORTED_CAPABILITIES[capability] &&
|
|
675
|
+
((capability !== "session.persistence" && capability !== "session.revert.conversation") ||
|
|
676
|
+
runtime.supportsPersistence),
|
|
677
|
+
);
|
|
678
|
+
const listeners = new Set<(event: ProviderEvent) => void>();
|
|
679
|
+
const sessions = new Map<
|
|
680
|
+
string,
|
|
681
|
+
{
|
|
682
|
+
token: symbol;
|
|
683
|
+
session: OmpProviderSession;
|
|
684
|
+
nativeSessionId?: string;
|
|
685
|
+
removeBrowserAuthorization?: () => void;
|
|
686
|
+
}
|
|
687
|
+
>();
|
|
688
|
+
const opening = new Map<
|
|
689
|
+
string,
|
|
690
|
+
{ token: symbol; controller: AbortController; settled: Promise<void> }
|
|
691
|
+
>();
|
|
692
|
+
const failedCleanup = new Map<
|
|
693
|
+
string,
|
|
694
|
+
{ token: symbol; nativeSessionId?: string; cleanup?: Promise<void> }
|
|
695
|
+
>();
|
|
696
|
+
const shutdown = new AbortController();
|
|
697
|
+
let catalogCleanup: Promise<void> | null = null;
|
|
698
|
+
const activeOperations = new Set<Promise<void>>();
|
|
699
|
+
let closing = false;
|
|
700
|
+
let closed = false;
|
|
701
|
+
let closePromise: Promise<void> | null = null;
|
|
702
|
+
|
|
703
|
+
const emit = (event: ProviderEvent) => {
|
|
704
|
+
if (closed) return;
|
|
705
|
+
for (const listener of listeners) listener(event);
|
|
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
|
+
};
|
|
714
|
+
|
|
715
|
+
const requestFailure = (
|
|
716
|
+
requestId: string,
|
|
717
|
+
error: unknown,
|
|
718
|
+
fallback = "OMP provider request failed",
|
|
719
|
+
) => {
|
|
720
|
+
emit({ type: "request.failed", requestId, error: errorDetails(error, fallback) });
|
|
721
|
+
};
|
|
722
|
+
const quarantineFailedCleanup = (
|
|
723
|
+
sessionId: string,
|
|
724
|
+
token: symbol,
|
|
725
|
+
error: unknown,
|
|
726
|
+
nativeSessionId?: string,
|
|
727
|
+
) => {
|
|
728
|
+
const cleanupFailure = isOmpCleanupFailure(error) ? error : undefined;
|
|
729
|
+
const cleanup = cleanupFailure?.cleanup ?? Promise.reject(error);
|
|
730
|
+
void cleanup.catch(() => undefined);
|
|
731
|
+
const quarantinedNativeSessionId = nativeSessionId ?? cleanupFailure?.nativeSessionId;
|
|
732
|
+
failedCleanup.set(sessionId, {
|
|
733
|
+
token,
|
|
734
|
+
cleanup,
|
|
735
|
+
...(quarantinedNativeSessionId ? { nativeSessionId: quarantinedNativeSessionId } : {}),
|
|
736
|
+
});
|
|
737
|
+
nativeReservations.quarantine(quarantinedNativeSessionId, token, cleanup, () => {
|
|
738
|
+
if (failedCleanup.get(sessionId)?.token === token) failedCleanup.delete(sessionId);
|
|
739
|
+
});
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const dispatch = async (input: ProviderInput): Promise<void> => {
|
|
743
|
+
switch (input.type) {
|
|
744
|
+
case "catalog":
|
|
745
|
+
if (catalogCleanup) {
|
|
746
|
+
requestFailure(input.requestId, new OmpPublicError("OMP catalog cleanup is incomplete"));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const configuredInput = input as typeof input & ProviderConfigurationCompat;
|
|
751
|
+
const catalogOptions = normalizeOmpCatalogOptions(
|
|
752
|
+
{
|
|
753
|
+
scope: input.cwd ? "workspace" : "global",
|
|
754
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
755
|
+
providerOptions: configuredInput.providerOptions,
|
|
756
|
+
settings: configuredInput.settings,
|
|
757
|
+
},
|
|
758
|
+
input.cwd ?? homedir(),
|
|
759
|
+
);
|
|
760
|
+
const catalog = await discoverOmpCatalog(
|
|
761
|
+
runtime,
|
|
762
|
+
catalogOptions,
|
|
763
|
+
shutdown.signal,
|
|
764
|
+
environment,
|
|
765
|
+
);
|
|
766
|
+
emit({
|
|
767
|
+
type: "catalog",
|
|
768
|
+
requestId: input.requestId,
|
|
769
|
+
catalog: safeCapabilities.includes("permission")
|
|
770
|
+
? catalog
|
|
771
|
+
: {
|
|
772
|
+
...catalog,
|
|
773
|
+
modes: catalog.modes.filter((mode) => mode.id === "full"),
|
|
774
|
+
defaultMode: "full",
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (isOmpCleanupFailure(error)) catalogCleanup = error.cleanup;
|
|
779
|
+
if (!closing) requestFailure(input.requestId, error, "OMP catalog discovery failed");
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
case "sessions":
|
|
783
|
+
try {
|
|
784
|
+
await nativeReservations.waitUntilListable(shutdown.signal);
|
|
785
|
+
const configuredInput = input as typeof input & ProviderConfigurationCompat;
|
|
786
|
+
const listingConfig = normalizeOmpCatalogOptions(
|
|
787
|
+
{
|
|
788
|
+
scope: input.cwd ? "workspace" : "global",
|
|
789
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
790
|
+
providerOptions: configuredInput.providerOptions,
|
|
791
|
+
settings: configuredInput.settings,
|
|
792
|
+
},
|
|
793
|
+
input.cwd ?? homedir(),
|
|
794
|
+
);
|
|
795
|
+
emit({
|
|
796
|
+
type: "sessions",
|
|
797
|
+
requestId: input.requestId,
|
|
798
|
+
sessions: (
|
|
799
|
+
await runtime.listSessions({
|
|
800
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
801
|
+
query: input.query,
|
|
802
|
+
limit: input.limit,
|
|
803
|
+
sessionDir: listingConfig.sessionDir,
|
|
804
|
+
})
|
|
805
|
+
).map((session) => ({
|
|
806
|
+
persistence: { version: 1, data: { sessionId: session.id } },
|
|
807
|
+
cwd: session.cwd,
|
|
808
|
+
...(session.title ? { title: session.title } : {}),
|
|
809
|
+
...(session.updatedAt ? { updatedAt: session.updatedAt } : {}),
|
|
810
|
+
...(session.firstPromptPreview
|
|
811
|
+
? { firstPromptPreview: session.firstPromptPreview }
|
|
812
|
+
: {}),
|
|
813
|
+
...(session.lastPromptPreview
|
|
814
|
+
? { lastPromptPreview: session.lastPromptPreview }
|
|
815
|
+
: {}),
|
|
816
|
+
})),
|
|
817
|
+
});
|
|
818
|
+
} catch (error) {
|
|
819
|
+
requestFailure(input.requestId, error, "OMP session listing failed");
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
case "session.open": {
|
|
823
|
+
if (
|
|
824
|
+
sessions.has(input.sessionId) ||
|
|
825
|
+
opening.has(input.sessionId) ||
|
|
826
|
+
failedCleanup.has(input.sessionId)
|
|
827
|
+
) {
|
|
828
|
+
requestFailure(input.requestId, new OmpPublicError("OMP session already exists"));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (sessions.size + opening.size + failedCleanup.size >= MAX_CONNECTION_SESSIONS) {
|
|
832
|
+
requestFailure(input.requestId, new OmpPublicError("OMP session limit reached"));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const token = Symbol(input.sessionId);
|
|
836
|
+
const controller = new AbortController();
|
|
837
|
+
const openingSettled = Promise.withResolvers<void>();
|
|
838
|
+
opening.set(input.sessionId, { token, controller, settled: openingSettled.promise });
|
|
839
|
+
let nativeSessionId: string | undefined;
|
|
840
|
+
let persistentOpening = false;
|
|
841
|
+
let session: OmpProviderSession | undefined;
|
|
842
|
+
try {
|
|
843
|
+
nativeSessionId = ompPersistenceSessionId(input);
|
|
844
|
+
if (nativeSessionId) {
|
|
845
|
+
await nativeReservations.reserve(nativeSessionId, token, controller.signal);
|
|
846
|
+
} else if (input.config.persist) {
|
|
847
|
+
await nativeReservations.beginPersistentOpen(token, controller.signal);
|
|
848
|
+
persistentOpening = true;
|
|
849
|
+
} else {
|
|
850
|
+
nativeReservations.assertOpenable();
|
|
851
|
+
}
|
|
852
|
+
const sessionEmit = (event: ProviderEvent) => {
|
|
853
|
+
if (
|
|
854
|
+
opening.get(input.sessionId)?.token !== token &&
|
|
855
|
+
sessions.get(input.sessionId)?.token !== token
|
|
856
|
+
) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
emit(event);
|
|
860
|
+
};
|
|
861
|
+
const transitionNativeSession = (previousSessionId: string, nextSessionId: string) => {
|
|
862
|
+
if (input.config.persist) {
|
|
863
|
+
if (nativeSessionId !== previousSessionId) {
|
|
864
|
+
throw new OmpPublicError("OMP native session ownership changed during rewind");
|
|
865
|
+
}
|
|
866
|
+
nativeReservations.transition(previousSessionId, nextSessionId, token);
|
|
867
|
+
}
|
|
868
|
+
nativeSessionId = nextSessionId;
|
|
869
|
+
const slot = sessions.get(input.sessionId);
|
|
870
|
+
if (slot?.token === token) slot.nativeSessionId = nextSessionId;
|
|
871
|
+
};
|
|
872
|
+
const quarantineRewindCleanup = (cleanup: Promise<void>) => {
|
|
873
|
+
quarantineFailedCleanup(
|
|
874
|
+
input.sessionId,
|
|
875
|
+
token,
|
|
876
|
+
new OmpCleanupFailure(
|
|
877
|
+
"OMP rewind left native session cleanup unresolved",
|
|
878
|
+
cleanup,
|
|
879
|
+
nativeSessionId,
|
|
880
|
+
),
|
|
881
|
+
nativeSessionId,
|
|
882
|
+
);
|
|
883
|
+
};
|
|
884
|
+
const retireRewindSession = () => {
|
|
885
|
+
deleteSession(input.sessionId, token);
|
|
886
|
+
};
|
|
887
|
+
session = await OmpProviderSession.open(
|
|
888
|
+
input,
|
|
889
|
+
runtime,
|
|
890
|
+
safeCapabilities,
|
|
891
|
+
sessionEmit,
|
|
892
|
+
transitionNativeSession,
|
|
893
|
+
quarantineRewindCleanup,
|
|
894
|
+
retireRewindSession,
|
|
895
|
+
scheduler,
|
|
896
|
+
replayTimeoutMs,
|
|
897
|
+
controller.signal,
|
|
898
|
+
environment,
|
|
899
|
+
mcpConnector,
|
|
900
|
+
mcpInitializationTimeoutMs,
|
|
901
|
+
);
|
|
902
|
+
const discoveredNativeSessionId = session.persistenceSessionId;
|
|
903
|
+
if (discoveredNativeSessionId) nativeSessionId = discoveredNativeSessionId;
|
|
904
|
+
if (persistentOpening) {
|
|
905
|
+
if (!nativeSessionId)
|
|
906
|
+
throw new OmpPublicError("OMP native session identity is missing");
|
|
907
|
+
nativeReservations.completePersistentOpen(nativeSessionId, token);
|
|
908
|
+
persistentOpening = false;
|
|
909
|
+
} else if (
|
|
910
|
+
discoveredNativeSessionId &&
|
|
911
|
+
discoveredNativeSessionId !== ompPersistenceSessionId(input)
|
|
912
|
+
) {
|
|
913
|
+
await nativeReservations.reserve(discoveredNativeSessionId, token, controller.signal);
|
|
914
|
+
}
|
|
915
|
+
if (
|
|
916
|
+
closing ||
|
|
917
|
+
controller.signal.aborted ||
|
|
918
|
+
opening.get(input.sessionId)?.token !== token
|
|
919
|
+
) {
|
|
920
|
+
await session.abortOpen();
|
|
921
|
+
nativeReservations.release(nativeSessionId, token);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
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
|
+
});
|
|
942
|
+
await session.publishOpened(input.requestId);
|
|
943
|
+
if (
|
|
944
|
+
closing ||
|
|
945
|
+
controller.signal.aborted ||
|
|
946
|
+
opening.get(input.sessionId)?.token !== token
|
|
947
|
+
) {
|
|
948
|
+
deleteSession(input.sessionId, token);
|
|
949
|
+
await session.close();
|
|
950
|
+
nativeReservations.release(nativeSessionId, token);
|
|
951
|
+
}
|
|
952
|
+
} catch (error) {
|
|
953
|
+
deleteSession(input.sessionId, token);
|
|
954
|
+
let cleanupError: unknown;
|
|
955
|
+
if (session) {
|
|
956
|
+
try {
|
|
957
|
+
await session.abortOpen();
|
|
958
|
+
} catch (failure) {
|
|
959
|
+
cleanupError = failure;
|
|
960
|
+
}
|
|
961
|
+
} else if (isOmpCleanupFailure(error)) {
|
|
962
|
+
cleanupError = error;
|
|
963
|
+
nativeSessionId ??= error.nativeSessionId;
|
|
964
|
+
}
|
|
965
|
+
if (cleanupError) {
|
|
966
|
+
quarantineFailedCleanup(input.sessionId, token, cleanupError, nativeSessionId);
|
|
967
|
+
} else {
|
|
968
|
+
nativeReservations.release(nativeSessionId, token);
|
|
969
|
+
}
|
|
970
|
+
if (!closing && opening.get(input.sessionId)?.token === token) {
|
|
971
|
+
const details = errorDetails(error, "OMP session failed to open");
|
|
972
|
+
emit({ type: "request.failed", requestId: input.requestId, error: details });
|
|
973
|
+
emit({ type: "session.closed", sessionId: input.sessionId, error: details });
|
|
974
|
+
}
|
|
975
|
+
} finally {
|
|
976
|
+
if (opening.get(input.sessionId)?.token === token) opening.delete(input.sessionId);
|
|
977
|
+
openingSettled.resolve();
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
case "session.prompt": {
|
|
982
|
+
const session = sessions.get(input.sessionId)?.session;
|
|
983
|
+
if (!session) {
|
|
984
|
+
emit({
|
|
985
|
+
type: "session.prompt_result",
|
|
986
|
+
sessionId: input.sessionId,
|
|
987
|
+
clientMessageId: input.prompt.clientMessageId,
|
|
988
|
+
result: {
|
|
989
|
+
type: "failed",
|
|
990
|
+
error: { message: "Unknown OMP session" },
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
await session.prompt(input);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
case "session.permission": {
|
|
999
|
+
const session = sessions.get(input.sessionId)?.session;
|
|
1000
|
+
if (!session) throw new OmpPublicError("Unknown OMP session");
|
|
1001
|
+
await session.permission(input);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
case "session.configure": {
|
|
1005
|
+
const session = sessions.get(input.sessionId)?.session;
|
|
1006
|
+
if (!session) {
|
|
1007
|
+
requestFailure(input.requestId, new OmpPublicError("Unknown OMP session"));
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
await session.configure(input);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
case "session.revert": {
|
|
1014
|
+
const session = sessions.get(input.sessionId)?.session;
|
|
1015
|
+
if (!session) {
|
|
1016
|
+
requestFailure(input.requestId, new OmpPublicError("Unknown OMP session"));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
await session.revert(input);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
case "session.interrupt": {
|
|
1023
|
+
const session = sessions.get(input.sessionId)?.session;
|
|
1024
|
+
if (!session) {
|
|
1025
|
+
requestFailure(input.requestId, new OmpPublicError("Unknown OMP session"));
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
await session.interrupt(input);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
case "session.close": {
|
|
1032
|
+
const slot = sessions.get(input.sessionId);
|
|
1033
|
+
if (!slot) {
|
|
1034
|
+
const pending = opening.get(input.sessionId);
|
|
1035
|
+
if (!pending) {
|
|
1036
|
+
requestFailure(input.requestId, new OmpPublicError("Unknown OMP session"));
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
pending.controller.abort(new OmpPublicError("OMP session open was canceled"));
|
|
1040
|
+
await pending.settled;
|
|
1041
|
+
emit({ type: "request.completed", requestId: input.requestId });
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
await slot.session.close();
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
deleteSession(input.sessionId, slot.token);
|
|
1048
|
+
quarantineFailedCleanup(input.sessionId, slot.token, error, slot.nativeSessionId);
|
|
1049
|
+
throw new OmpPublicError("OMP session close failed");
|
|
1050
|
+
}
|
|
1051
|
+
deleteSession(input.sessionId, slot.token);
|
|
1052
|
+
nativeReservations.release(slot.nativeSessionId, slot.token);
|
|
1053
|
+
emit({ type: "request.completed", requestId: input.requestId });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
default:
|
|
1057
|
+
if ("requestId" in input) {
|
|
1058
|
+
requestFailure(input.requestId, new OmpPublicError("Unsupported provider request"));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
const disposeConnection = async (): Promise<void> => {
|
|
1063
|
+
closing = true;
|
|
1064
|
+
const shutdownReason = new Error("OMP provider connection closed");
|
|
1065
|
+
shutdown.abort(shutdownReason);
|
|
1066
|
+
for (const { controller } of opening.values()) controller.abort(shutdownReason);
|
|
1067
|
+
for (const { session } of sessions.values()) session.beginConnectionShutdown();
|
|
1068
|
+
for (const slot of sessions.values()) slot.removeBrowserAuthorization?.();
|
|
1069
|
+
const failures: unknown[] = [];
|
|
1070
|
+
const operationBatch = [...activeOperations];
|
|
1071
|
+
const operationResults = await Promise.allSettled(operationBatch);
|
|
1072
|
+
for (const operation of operationBatch) activeOperations.delete(operation);
|
|
1073
|
+
for (const result of operationResults) {
|
|
1074
|
+
if (result.status === "rejected") failures.push(result.reason);
|
|
1075
|
+
}
|
|
1076
|
+
const seenSessions = new Set<OmpProviderSession>();
|
|
1077
|
+
const seenCleanups = new Set<Promise<void>>();
|
|
1078
|
+
const cleanupBatch: Promise<void>[] = [];
|
|
1079
|
+
for (const [sessionId, { session, nativeSessionId, token }] of sessions) {
|
|
1080
|
+
if (seenSessions.has(session)) continue;
|
|
1081
|
+
seenSessions.add(session);
|
|
1082
|
+
cleanupBatch.push(
|
|
1083
|
+
Promise.resolve().then(async () => {
|
|
1084
|
+
try {
|
|
1085
|
+
await session.close();
|
|
1086
|
+
nativeReservations.release(nativeSessionId, token);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
quarantineFailedCleanup(sessionId, token, error, nativeSessionId);
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}),
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
for (const { cleanup } of failedCleanup.values()) {
|
|
1095
|
+
if (!cleanup || seenCleanups.has(cleanup)) continue;
|
|
1096
|
+
seenCleanups.add(cleanup);
|
|
1097
|
+
cleanupBatch.push(cleanup);
|
|
1098
|
+
}
|
|
1099
|
+
if (catalogCleanup && !seenCleanups.has(catalogCleanup)) {
|
|
1100
|
+
seenCleanups.add(catalogCleanup);
|
|
1101
|
+
cleanupBatch.push(catalogCleanup);
|
|
1102
|
+
}
|
|
1103
|
+
const results = await Promise.allSettled(cleanupBatch);
|
|
1104
|
+
for (const result of results) {
|
|
1105
|
+
if (result.status === "rejected") failures.push(result.reason);
|
|
1106
|
+
}
|
|
1107
|
+
listeners.clear();
|
|
1108
|
+
closed = true;
|
|
1109
|
+
if (failures.length > 0) {
|
|
1110
|
+
throw new AggregateError(failures, "OMP provider connection cleanup failed");
|
|
1111
|
+
}
|
|
1112
|
+
sessions.clear();
|
|
1113
|
+
opening.clear();
|
|
1114
|
+
failedCleanup.clear();
|
|
1115
|
+
catalogCleanup = null;
|
|
1116
|
+
};
|
|
1117
|
+
return {
|
|
1118
|
+
version: 1,
|
|
1119
|
+
capabilities: safeCapabilities,
|
|
1120
|
+
async send(input) {
|
|
1121
|
+
if (closing || closed) throw new Error("OMP provider connection is closed");
|
|
1122
|
+
if (activeOperations.size >= MAX_ACTIVE_OPERATIONS) {
|
|
1123
|
+
throw new OmpPublicError("OMP provider is busy");
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
preflightProviderInput(input);
|
|
1127
|
+
input = parseProviderInputCompat(input);
|
|
1128
|
+
validateInputEnvelope(input);
|
|
1129
|
+
requireProviderCapabilities(safeCapabilities, input);
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
const raw = input as unknown;
|
|
1132
|
+
if (raw && typeof raw === "object") {
|
|
1133
|
+
const record = raw as Record<string, unknown>;
|
|
1134
|
+
if (record.type === "session.prompt" && isBoundedIdentifier(record.sessionId)) {
|
|
1135
|
+
const prompt = record.prompt;
|
|
1136
|
+
if (prompt && typeof prompt === "object") {
|
|
1137
|
+
const clientMessageId = (prompt as Record<string, unknown>).clientMessageId;
|
|
1138
|
+
if (isBoundedIdentifier(clientMessageId)) {
|
|
1139
|
+
emit({
|
|
1140
|
+
type: "session.prompt_result",
|
|
1141
|
+
sessionId: record.sessionId,
|
|
1142
|
+
clientMessageId,
|
|
1143
|
+
result: { type: "failed", error: errorDetails(error, "OMP prompt failed") },
|
|
1144
|
+
});
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (isBoundedIdentifier(record.requestId)) {
|
|
1150
|
+
requestFailure(record.requestId, error, "OMP provider request failed");
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
const operation = Promise.resolve()
|
|
1157
|
+
.then(async () => {
|
|
1158
|
+
if (closing || closed) {
|
|
1159
|
+
if (input.type === "session.permission") {
|
|
1160
|
+
throw new Error("OMP provider connection is closed");
|
|
1161
|
+
}
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
await dispatch(input);
|
|
1165
|
+
})
|
|
1166
|
+
.catch((error) => {
|
|
1167
|
+
if (input.type === "session.prompt") {
|
|
1168
|
+
emit({
|
|
1169
|
+
type: "session.prompt_result",
|
|
1170
|
+
sessionId: input.sessionId,
|
|
1171
|
+
clientMessageId: input.prompt.clientMessageId,
|
|
1172
|
+
result: { type: "failed", error: errorDetails(error, "OMP prompt failed") },
|
|
1173
|
+
});
|
|
1174
|
+
} else if (input.type === "session.permission") {
|
|
1175
|
+
throw error;
|
|
1176
|
+
} else if ("requestId" in input) {
|
|
1177
|
+
requestFailure(input.requestId, error, "OMP provider request failed");
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
activeOperations.add(operation);
|
|
1181
|
+
void operation.then(
|
|
1182
|
+
() => activeOperations.delete(operation),
|
|
1183
|
+
() => activeOperations.delete(operation),
|
|
1184
|
+
);
|
|
1185
|
+
if (input.type === "session.permission") await operation;
|
|
1186
|
+
},
|
|
1187
|
+
onEvent(listener) {
|
|
1188
|
+
listeners.add(listener);
|
|
1189
|
+
return () => listeners.delete(listener);
|
|
1190
|
+
},
|
|
1191
|
+
close() {
|
|
1192
|
+
closePromise ??= Promise.resolve().then(disposeConnection);
|
|
1193
|
+
return closePromise;
|
|
1194
|
+
},
|
|
1195
|
+
};
|
|
1196
|
+
}
|