@omercnet/paseo-omp 0.2.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 (63) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/SUPPORT.md +40 -0
  5. package/TESTING.md +147 -0
  6. package/client/hub-icon.tsx +12 -0
  7. package/client/hub-popover.tsx +132 -0
  8. package/client/hub-status.ts +29 -0
  9. package/client/memory-panel.tsx +71 -0
  10. package/client/memory-popover.tsx +70 -0
  11. package/client/omp-config-surface.tsx +1274 -0
  12. package/client/omp-doc-links.ts +117 -0
  13. package/client/omp-plugin-manager.tsx +833 -0
  14. package/client/provider-diagnostics-state.ts +250 -0
  15. package/client/provider-icon.tsx +27 -0
  16. package/client/provider-image.tsx +66 -0
  17. package/client/quota-popover.tsx +150 -0
  18. package/client/quota-state.ts +131 -0
  19. package/client/sessions-popover.tsx +73 -0
  20. package/docs/alpha-release-checklist.md +70 -0
  21. package/docs/configuration.md +122 -0
  22. package/docs/core-provider-issue-audit.md +108 -0
  23. package/docs/installation.md +73 -0
  24. package/index.client.tsx +272 -0
  25. package/index.server.ts +51 -0
  26. package/package.json +84 -0
  27. package/paseo-plugin.json +5 -0
  28. package/server/hub.ts +145 -0
  29. package/server/memory.ts +86 -0
  30. package/server/mutation-queue.ts +12 -0
  31. package/server/omp-config.ts +126 -0
  32. package/server/omp-plugins.ts +627 -0
  33. package/server/omp-settings.ts +291 -0
  34. package/server/paths.ts +64 -0
  35. package/server/provider/catalog.ts +173 -0
  36. package/server/provider/config-normalization.ts +148 -0
  37. package/server/provider/connection.ts +992 -0
  38. package/server/provider/host-tools.ts +706 -0
  39. package/server/provider/image.ts +143 -0
  40. package/server/provider/mcp-transport.ts +394 -0
  41. package/server/provider/omp-rpc.ts +2739 -0
  42. package/server/provider/omp.svg +5 -0
  43. package/server/provider/provider-options.ts +27 -0
  44. package/server/provider/registration.ts +151 -0
  45. package/server/provider/security.ts +317 -0
  46. package/server/provider/session-descriptors.ts +431 -0
  47. package/server/provider/session.ts +4451 -0
  48. package/server/provider/settings.ts +78 -0
  49. package/server/provider/subsessions.ts +847 -0
  50. package/server/provider/timeline-projector.ts +1764 -0
  51. package/server/provider-diagnostics.ts +1057 -0
  52. package/server/quota.ts +54 -0
  53. package/server/sessions.ts +58 -0
  54. package/shared/hub.ts +43 -0
  55. package/shared/memory.ts +23 -0
  56. package/shared/omp-config.ts +81 -0
  57. package/shared/omp-plugins.ts +223 -0
  58. package/shared/omp-settings.ts +207 -0
  59. package/shared/provider-diagnostics.ts +117 -0
  60. package/shared/provider-image.ts +160 -0
  61. package/shared/quota.ts +22 -0
  62. package/shared/sessions.ts +23 -0
  63. package/tsconfig.json +16 -0
@@ -0,0 +1,2739 @@
1
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { z } from "zod";
5
+ import { isValidImagePayload } from "./image";
6
+ import { boundedJsonBytes, OmpCleanupFailure, OmpPublicError, utf8Bytes } from "./security";
7
+ import {
8
+ listOmpSessionDescriptors,
9
+ type OmpSessionDescriptor,
10
+ type OmpSessionListOptions,
11
+ readOmpPersistedSubagentTranscript,
12
+ validateNativeSessionId,
13
+ } from "./session-descriptors";
14
+ import type { OmpOutputRedaction } from "./settings";
15
+
16
+ const READY_TIMEOUT_MS = 20_000;
17
+ const REQUEST_TIMEOUT_MS = 60_000;
18
+ const PROCESS_STOP_TIMEOUT_MS = 750;
19
+ const CHUNK_STALE_MS = 30_000;
20
+ const MAX_PHYSICAL_FRAME_BYTES = 1024 * 1024;
21
+ const MAX_CHUNK_BYTES = 256 * 1024;
22
+ const MAX_ENCODED_CHUNK_BYTES = Math.ceil(MAX_CHUNK_BYTES / 3) * 4;
23
+ const MAX_REASSEMBLED_FRAME_BYTES = 64 * 1024 * 1024;
24
+ const MAX_SEMANTIC_FRAME_BYTES = 12 * 1024 * 1024;
25
+ const MAX_CHUNK_COUNT = MAX_REASSEMBLED_FRAME_BYTES / MAX_CHUNK_BYTES;
26
+ const MAX_ID_LENGTH = 256;
27
+ const MAX_NAME_LENGTH = 256;
28
+ const MAX_MODEL_SELECTOR_BYTES = MAX_NAME_LENGTH * 2 + 1;
29
+ const MAX_CONFIG_EVENT_TEXT_BYTES = 64 * 1024;
30
+ const MAX_TEXT_LENGTH = 1024 * 1024;
31
+ const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
32
+ const MAX_SYSTEM_PROMPT_LENGTH = 64 * 1024;
33
+ const MAX_IMAGE_DATA_LENGTH = 8 * 1024 * 1024;
34
+ const MAX_TOOL_PAYLOAD_LENGTH = 256 * 1024;
35
+ const MAX_ACTIVE_TOOLS = 64;
36
+ const MAX_HOST_TOOLS = 256;
37
+ const MAX_TOOL_APPROVAL_FRAME_BYTES = 64 * 1024;
38
+ const MAX_TOOL_APPROVAL_STRING_BYTES = 8 * 1024;
39
+ const MAX_TOOL_APPROVAL_COLLECTION_ITEMS = 32;
40
+ const MAX_TOOL_APPROVAL_INPUT_NODES = 256;
41
+ const MAX_TOOL_APPROVAL_DEPTH = 4;
42
+ const MAX_TOOL_APPROVAL_ID_BYTES = 512;
43
+ const MAX_TOOL_APPROVAL_NAME_BYTES = 256;
44
+ const MAX_TOOL_APPROVAL_DETAIL_LINES = 16;
45
+ const MAX_TOOL_APPROVAL_DETAIL_BYTES = 2 * 1024;
46
+ const MAX_TOOL_APPROVAL_METADATA_FIELDS = 33;
47
+ const MAX_TOOL_APPROVAL_METADATA_FIELD_BYTES = 64;
48
+ const MAX_TOOL_APPROVAL_TIMEOUT_MS = 24 * 60 * 60 * 1000;
49
+ const MAX_TOOL_APPROVAL_PATH_BYTES = 4 * 1024;
50
+ const MAX_TOOL_APPROVAL_CONTENT_BYTES = 20 * 1024;
51
+ type TimerHandle = ReturnType<typeof setTimeout>;
52
+ const MAX_PENDING_REQUESTS = 256;
53
+ const MAX_PENDING_ONE_WAY_WRITES = 256;
54
+ const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
55
+ const MAX_LINE_PARTS = 4_096;
56
+ const MAX_ARRAY_ITEMS = 512;
57
+ const MAX_CONTENT_PARTS = 64;
58
+ const MAX_TODOS = 256;
59
+ const MAX_ENV_ENTRIES = 256;
60
+ const MAX_ENV_VALUE_LENGTH = 64 * 1024;
61
+ const MAX_ENV_TOTAL_LENGTH = 1024 * 1024;
62
+ const MAX_PATH_LENGTH = 4_096;
63
+ const WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows";
64
+ const MAX_TOKEN_COUNT = Number.MAX_SAFE_INTEGER;
65
+ const MAX_COST_USD = 1_000_000_000;
66
+ const MAX_CONTEXT_PERCENT = 1_000_000;
67
+ function boundedJsonString(maxBytes: number, minBytes = 0) {
68
+ return z.string().refine((value) => {
69
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8") - 2;
70
+ return bytes >= minBytes && bytes <= maxBytes;
71
+ });
72
+ }
73
+ function isBoundedToolApprovalId(value: unknown): value is string {
74
+ return typeof value === "string" && value.length > 0 && Buffer.byteLength(value, "utf8") <= 512;
75
+ }
76
+
77
+ export const OMP_HOST_TOOL_FRAME_LIMIT_ERROR =
78
+ "MCP host tool result exceeds the OMP RPC frame limit";
79
+ const MIN_HOST_TOOL_RESULT_FRAME_BYTES = Buffer.byteLength(
80
+ `${JSON.stringify({
81
+ type: "host_tool_result",
82
+ id: "\0".repeat(MAX_ID_LENGTH),
83
+ result: {
84
+ content: [{ type: "text", text: OMP_HOST_TOOL_FRAME_LIMIT_ERROR }],
85
+ details: {},
86
+ isError: true,
87
+ },
88
+ isError: true,
89
+ })}\n`,
90
+ );
91
+ function boundedString(maxBytes: number, minBytes = 0) {
92
+ return z.string().refine((value) => {
93
+ const bytes = utf8Bytes(value);
94
+ return bytes >= minBytes && bytes <= maxBytes;
95
+ });
96
+ }
97
+
98
+ const IDENTIFIER = boundedString(MAX_ID_LENGTH, 1);
99
+ const NAME = boundedString(MAX_NAME_LENGTH, 1);
100
+ const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
101
+ const TEXT = boundedString(MAX_TEXT_LENGTH);
102
+ const OmpThinkingLevelSchema = z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
103
+
104
+ function isBoundedJson(
105
+ value: unknown,
106
+ maxBytes = MAX_TOOL_PAYLOAD_LENGTH,
107
+ maxItems = MAX_ARRAY_ITEMS,
108
+ maxNodes = 2_048,
109
+ ): boolean {
110
+ return (
111
+ boundedJsonBytes(value, maxBytes, maxItems, maxBytes, maxNodes) !== Number.POSITIVE_INFINITY
112
+ );
113
+ }
114
+
115
+ const OmpContentPartSchema = z
116
+ .object({
117
+ type: NAME,
118
+ text: TEXT.optional(),
119
+ thinking: TEXT.optional(),
120
+ data: boundedString(MAX_IMAGE_DATA_LENGTH).optional(),
121
+ mimeType: boundedString(128).optional(),
122
+ id: IDENTIFIER.optional(),
123
+ name: NAME.optional(),
124
+ arguments: z
125
+ .unknown()
126
+ .refine((value) => isBoundedJson(value, MAX_TOOL_PAYLOAD_LENGTH, 1_024, 4_096))
127
+ .optional(),
128
+ })
129
+ .superRefine((part, context) => {
130
+ if (part.type === "toolCall" && (!part.id || !part.name || part.arguments === undefined)) {
131
+ context.addIssue({ code: "custom", message: "invalid tool call payload" });
132
+ return;
133
+ }
134
+ if (part.type !== "image") return;
135
+ if (
136
+ part.data === undefined ||
137
+ part.mimeType === undefined ||
138
+ !isValidImagePayload(part.data, part.mimeType, MAX_IMAGE_DATA_LENGTH)
139
+ ) {
140
+ context.addIssue({ code: "custom", message: "invalid image payload" });
141
+ }
142
+ });
143
+ const OmpDisplayContentSchema = z.union([
144
+ TEXT,
145
+ z.array(OmpContentPartSchema).max(MAX_CONTENT_PARTS),
146
+ ]);
147
+ const OmpImageArraySchema = z
148
+ .array(OmpContentPartSchema)
149
+ .max(MAX_CONTENT_PARTS)
150
+ .superRefine((parts, context) => {
151
+ if (parts.some((part) => part.type !== "image")) {
152
+ context.addIssue({ code: "custom", message: "invalid image collection" });
153
+ }
154
+ });
155
+ const OmpMessageIdentityShape = {
156
+ id: IDENTIFIER.optional(),
157
+ entryId: IDENTIFIER.optional(),
158
+ responseId: IDENTIFIER.optional(),
159
+ images: OmpImageArraySchema.optional(),
160
+ timestamp: z.number().finite().optional(),
161
+ details: z
162
+ .unknown()
163
+ .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096))
164
+ .optional(),
165
+ };
166
+ type OmpContentPart = z.infer<typeof OmpContentPartSchema>;
167
+ type OmpMessageIdentity = {
168
+ id?: string;
169
+ entryId?: string;
170
+ responseId?: string;
171
+ images?: OmpContentPart[];
172
+ timestamp?: number;
173
+ details?: unknown;
174
+ display?: boolean;
175
+ customType?: string;
176
+ content?: unknown;
177
+ command?: string;
178
+ output?: string;
179
+ exitCode?: number | null;
180
+ cancelled?: boolean;
181
+ truncated?: boolean;
182
+ };
183
+ export type OmpMessage = OmpMessageIdentity &
184
+ (
185
+ | {
186
+ role: "assistant";
187
+ content?: string | OmpContentPart[];
188
+ errorMessage?: string | null;
189
+ stopReason?: string;
190
+ }
191
+ | { role: "user"; content: string | OmpContentPart[] }
192
+ | {
193
+ role: "toolResult";
194
+ toolCallId: string;
195
+ toolName: string;
196
+ content: unknown;
197
+ details?: unknown;
198
+ isError?: boolean;
199
+ }
200
+ | {
201
+ role: "bashExecution";
202
+ command: string;
203
+ output?: string;
204
+ exitCode?: number | null;
205
+ cancelled?: boolean;
206
+ truncated?: boolean;
207
+ }
208
+ | { role: "custom"; customType?: string; content?: unknown; display?: boolean }
209
+ );
210
+
211
+ const OmpMessageSchema: z.ZodType<OmpMessage> = z.union([
212
+ z.object({
213
+ role: z.literal("assistant"),
214
+ content: OmpDisplayContentSchema.optional(),
215
+ ...OmpMessageIdentityShape,
216
+ errorMessage: boundedString(4_096).nullable().optional(),
217
+ stopReason: boundedString(64).optional(),
218
+ }),
219
+ z.object({
220
+ role: z.literal("user"),
221
+ content: OmpDisplayContentSchema,
222
+ ...OmpMessageIdentityShape,
223
+ }),
224
+ z.object({
225
+ role: z.literal("toolResult"),
226
+ toolCallId: IDENTIFIER,
227
+ toolName: NAME,
228
+ content: z
229
+ .unknown()
230
+ .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192)),
231
+ isError: z.boolean().optional(),
232
+ ...OmpMessageIdentityShape,
233
+ }),
234
+ z.object({
235
+ role: z.literal("bashExecution"),
236
+ command: TEXT,
237
+ output: TEXT.optional(),
238
+ exitCode: z.number().int().nullable().optional(),
239
+ cancelled: z.boolean().optional(),
240
+ truncated: z.boolean().optional(),
241
+ ...OmpMessageIdentityShape,
242
+ }),
243
+ z.object({
244
+ role: z.literal("custom"),
245
+ customType: NAME.optional(),
246
+ content: z
247
+ .unknown()
248
+ .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 8_192))
249
+ .optional(),
250
+ display: z.boolean().optional(),
251
+ ...OmpMessageIdentityShape,
252
+ }),
253
+ ]);
254
+
255
+ const OmpAssistantMessageEventSchema = z
256
+ .object({
257
+ type: NAME,
258
+ contentIndex: z
259
+ .number()
260
+ .int()
261
+ .nonnegative()
262
+ .max(MAX_CONTENT_PARTS - 1)
263
+ .optional(),
264
+ delta: TEXT.optional(),
265
+ content: z
266
+ .unknown()
267
+ .refine((value) => isBoundedJson(value, MAX_IMAGE_DATA_LENGTH + 1_024))
268
+ .optional(),
269
+ })
270
+ .superRefine((event, context) => {
271
+ const content = event.content;
272
+ const imageLike =
273
+ content !== null &&
274
+ typeof content === "object" &&
275
+ !Array.isArray(content) &&
276
+ "type" in content &&
277
+ content.type === "image";
278
+ if (!event.type.startsWith("image_") && !imageLike) return;
279
+ const image = OmpContentPartSchema.safeParse(content);
280
+ if (!image.success || image.data.type !== "image") {
281
+ context.addIssue({ code: "custom", message: "invalid image event" });
282
+ }
283
+ });
284
+
285
+ const OmpAvailableCommandSchema = z.object({
286
+ name: NAME,
287
+ aliases: z.array(NAME).max(32).optional(),
288
+ description: boundedString(4_096).optional(),
289
+ input: z
290
+ .object({ hint: boundedString(1_024).optional() })
291
+ .nullable()
292
+ .optional(),
293
+ subcommands: z
294
+ .array(
295
+ z.object({
296
+ name: NAME,
297
+ description: boundedString(4_096).optional(),
298
+ usage: boundedString(1_024).optional(),
299
+ }),
300
+ )
301
+ .max(128)
302
+ .optional(),
303
+ source: boundedString(64).optional(),
304
+ });
305
+ const OmpModelSchema = z.object({
306
+ provider: OMP_PROVIDER_NAME,
307
+ id: NAME,
308
+ name: boundedString(MAX_NAME_LENGTH).optional(),
309
+ reasoning: z.boolean().optional(),
310
+ thinking: z
311
+ .object({
312
+ efforts: z.array(boundedString(32)).max(16).optional(),
313
+ defaultLevel: boundedString(32).optional(),
314
+ })
315
+ .optional(),
316
+ input: z.array(NAME).max(16).optional(),
317
+ contextWindow: z.number().int().nonnegative().max(100_000_000).nullable().optional(),
318
+ });
319
+ const TokenCountSchema = z.number().int().nonnegative().max(MAX_TOKEN_COUNT);
320
+ const OptionalTokenCountSchema = TokenCountSchema.nullable().optional();
321
+ const OptionalCostSchema = z
322
+ .number()
323
+ .finite()
324
+ .nonnegative()
325
+ .max(MAX_COST_USD)
326
+ .nullable()
327
+ .optional();
328
+ const OmpContextUsageSchema = z.object({
329
+ tokens: OptionalTokenCountSchema,
330
+ contextWindow: TokenCountSchema.max(100_000_000).nullable().optional(),
331
+ percent: z.number().finite().nonnegative().max(MAX_CONTEXT_PERCENT).nullable().optional(),
332
+ });
333
+ const OmpSessionStatsSchema = z.object({
334
+ userMessages: OptionalTokenCountSchema,
335
+ assistantMessages: OptionalTokenCountSchema,
336
+ toolCalls: OptionalTokenCountSchema,
337
+ toolResults: OptionalTokenCountSchema,
338
+ totalMessages: OptionalTokenCountSchema,
339
+ tokens: z
340
+ .object({
341
+ input: OptionalTokenCountSchema,
342
+ output: OptionalTokenCountSchema,
343
+ reasoning: OptionalTokenCountSchema,
344
+ cacheRead: OptionalTokenCountSchema,
345
+ cacheWrite: OptionalTokenCountSchema,
346
+ total: OptionalTokenCountSchema,
347
+ })
348
+ .nullable()
349
+ .optional(),
350
+ cost: OptionalCostSchema,
351
+ premiumRequests: OptionalTokenCountSchema,
352
+ credits: z
353
+ .object({
354
+ cost: OptionalCostSchema,
355
+ committedCost: OptionalCostSchema,
356
+ acuCost: OptionalCostSchema,
357
+ })
358
+ .nullable()
359
+ .optional(),
360
+ routedModels: z.record(NAME, OptionalTokenCountSchema).nullable().optional(),
361
+ contextUsage: OmpContextUsageSchema.nullable().optional(),
362
+ });
363
+ const OmpCompactionResultSchema = z.object({
364
+ tokensBefore: OptionalTokenCountSchema,
365
+ preTokens: OptionalTokenCountSchema,
366
+ });
367
+ const OmpSessionStateSchema = z.object({
368
+ model: OmpModelSchema.nullable().optional(),
369
+ thinkingLevel: OmpThinkingLevelSchema.optional(),
370
+ isStreaming: z.boolean(),
371
+ isCompacting: z.boolean(),
372
+ sessionId: IDENTIFIER,
373
+ autoCompactionEnabled: z.boolean().optional(),
374
+ contextUsage: OmpContextUsageSchema.nullable().optional(),
375
+ sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
376
+ });
377
+ const OmpReadyFrameSchema = z.object({
378
+ type: z.literal("ready"),
379
+ protocolVersion: z.number().int().positive().max(16).optional(),
380
+ supportedProtocolVersions: z.array(z.number().int().positive().max(16)).max(8).optional(),
381
+ maxFrameBytes: z.number().int().positive().optional(),
382
+ maxReassembledFrameBytes: z.number().int().positive().optional(),
383
+ features: z.record(z.string(), z.unknown()).optional(),
384
+ });
385
+ const OmpResponseFrameSchema = z.object({
386
+ type: z.literal("response"),
387
+ id: IDENTIFIER,
388
+ success: z.boolean(),
389
+ data: z.unknown().optional(),
390
+ error: boundedString(4_096).optional(),
391
+ });
392
+ const OmpChunkFrameSchema = z.object({
393
+ type: z.literal("rpc_chunk"),
394
+ chunkId: IDENTIFIER,
395
+ index: z.number().int().nonnegative(),
396
+ count: z.number().int().positive().max(MAX_CHUNK_COUNT),
397
+ byteLength: z.number().int().nonnegative().max(MAX_REASSEMBLED_FRAME_BYTES),
398
+ data: boundedString(MAX_ENCODED_CHUNK_BYTES),
399
+ });
400
+ const JsonObjectSchema = z.record(z.string(), z.unknown());
401
+ const BoundedToolPayloadSchema = z
402
+ .unknown()
403
+ .refine((value) => isBoundedJson(value, MAX_SEMANTIC_FRAME_BYTES, 1_024, 4_096));
404
+ const OmpHostToolDefinitionSchema = z.object({
405
+ name: NAME,
406
+ label: NAME.optional(),
407
+ description: boundedString(MAX_TEXT_LENGTH),
408
+ loadMode: z.enum(["essential", "discoverable"]).optional(),
409
+ parameters: JsonObjectSchema,
410
+ });
411
+ const OmpHostToolCallSchema = z.object({
412
+ type: z.literal("host_tool_call"),
413
+ id: IDENTIFIER,
414
+ toolCallId: IDENTIFIER,
415
+ toolName: NAME,
416
+ arguments: JsonObjectSchema,
417
+ });
418
+ const OmpHostToolCancelSchema = z.object({
419
+ type: z.literal("host_tool_cancel"),
420
+ id: IDENTIFIER,
421
+ targetId: IDENTIFIER,
422
+ });
423
+ const OmpHostToolContentSchema = z.object({ type: NAME, text: TEXT.optional() }).passthrough();
424
+ const OmpHostToolAgentResultSchema = z.object({
425
+ content: z.array(OmpHostToolContentSchema).max(MAX_ARRAY_ITEMS),
426
+ details: BoundedToolPayloadSchema.optional(),
427
+ isError: z.boolean().optional(),
428
+ });
429
+ const OmpHostToolResultSchema = z.object({
430
+ type: z.literal("host_tool_result"),
431
+ id: IDENTIFIER,
432
+ result: OmpHostToolAgentResultSchema,
433
+ isError: z.boolean().optional(),
434
+ });
435
+ const OmpHostToolUpdateSchema = z.object({
436
+ type: z.literal("host_tool_update"),
437
+ id: IDENTIFIER,
438
+ partialResult: OmpHostToolAgentResultSchema,
439
+ });
440
+ const OmpToolApprovalIdentitySchema = z.discriminatedUnion("kind", [
441
+ z.object({ kind: z.literal("shell"), command: boundedJsonString(24 * 1024, 1) }).strict(),
442
+ z
443
+ .object({
444
+ kind: z.literal("edit"),
445
+ paths: z.array(boundedJsonString(MAX_TOOL_APPROVAL_PATH_BYTES, 1)).min(1).max(16),
446
+ content: boundedJsonString(MAX_TOOL_APPROVAL_CONTENT_BYTES),
447
+ })
448
+ .strict(),
449
+ z
450
+ .object({
451
+ kind: z.literal("write"),
452
+ path: boundedJsonString(MAX_TOOL_APPROVAL_PATH_BYTES, 1),
453
+ content: boundedJsonString(MAX_TOOL_APPROVAL_CONTENT_BYTES),
454
+ })
455
+ .strict(),
456
+ z.object({ kind: z.literal("other") }).strict(),
457
+ ]);
458
+ type OmpToolApprovalValue =
459
+ | string
460
+ | number
461
+ | boolean
462
+ | null
463
+ | OmpToolApprovalValue[]
464
+ | { [key: string]: OmpToolApprovalValue };
465
+ const OmpToolApprovalValueSchema: z.ZodType<OmpToolApprovalValue> = z.lazy(() =>
466
+ z.union([
467
+ boundedString(MAX_TOOL_APPROVAL_STRING_BYTES),
468
+ z.number().finite(),
469
+ z.boolean(),
470
+ z.null(),
471
+ z.array(OmpToolApprovalValueSchema).max(MAX_TOOL_APPROVAL_COLLECTION_ITEMS),
472
+ z.record(boundedString(128, 1), OmpToolApprovalValueSchema),
473
+ ]),
474
+ );
475
+ function approvalInputWithinBounds(value: unknown): boolean {
476
+ const pending: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
477
+ let nodes = 0;
478
+ while (pending.length > 0) {
479
+ const current = pending.pop();
480
+ if (!current) break;
481
+ nodes += 1;
482
+ if (nodes > MAX_TOOL_APPROVAL_INPUT_NODES) return false;
483
+ if (current.value === null || typeof current.value !== "object") continue;
484
+ if (current.depth >= MAX_TOOL_APPROVAL_DEPTH) return false;
485
+ for (const child of Array.isArray(current.value)
486
+ ? current.value
487
+ : Object.values(current.value as Record<string, unknown>)) {
488
+ pending.push({ value: child, depth: current.depth + 1 });
489
+ }
490
+ }
491
+ return true;
492
+ }
493
+ const OmpToolApprovalRequestSchema = z
494
+ .object({
495
+ type: z.literal("tool_approval_request"),
496
+ id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
497
+ toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
498
+ toolKind: z.enum(["shell", "edit", "write", "other"]),
499
+ toolName: boundedString(MAX_TOOL_APPROVAL_NAME_BYTES, 1),
500
+ tier: z.enum(["read", "write", "exec"]),
501
+ identity: OmpToolApprovalIdentitySchema,
502
+ input: z
503
+ .record(boundedString(128, 1), OmpToolApprovalValueSchema)
504
+ .refine(approvalInputWithinBounds),
505
+ detail: z
506
+ .object({
507
+ lines: z
508
+ .array(boundedString(MAX_TOOL_APPROVAL_DETAIL_BYTES))
509
+ .max(MAX_TOOL_APPROVAL_DETAIL_LINES),
510
+ truncated: z.boolean(),
511
+ truncatedFields: z
512
+ .array(boundedString(MAX_TOOL_APPROVAL_METADATA_FIELD_BYTES))
513
+ .max(MAX_TOOL_APPROVAL_METADATA_FIELDS),
514
+ redacted: z.boolean(),
515
+ redactedFields: z
516
+ .array(boundedString(MAX_TOOL_APPROVAL_METADATA_FIELD_BYTES))
517
+ .max(MAX_TOOL_APPROVAL_METADATA_FIELDS),
518
+ reason: boundedString(MAX_TOOL_APPROVAL_DETAIL_BYTES).optional(),
519
+ providerSafetyChecks: z
520
+ .array(boundedString(MAX_TOOL_APPROVAL_DETAIL_BYTES))
521
+ .max(MAX_TOOL_APPROVAL_DETAIL_LINES)
522
+ .optional(),
523
+ })
524
+ .strict(),
525
+ timeout: z.number().finite().nonnegative().max(MAX_TOOL_APPROVAL_TIMEOUT_MS).optional(),
526
+ })
527
+ .strict()
528
+ .superRefine((request, context) => {
529
+ if (request.toolKind !== request.identity.kind) {
530
+ context.addIssue({ code: "custom", message: "tool approval identity kind mismatch" });
531
+ }
532
+ if (Buffer.byteLength(JSON.stringify(request), "utf8") + 1 > MAX_TOOL_APPROVAL_FRAME_BYTES) {
533
+ context.addIssue({ code: "custom", message: "tool approval request exceeds bounds" });
534
+ }
535
+ });
536
+ const OmpToolApprovalCancelSchema = z
537
+ .object({
538
+ type: z.literal("tool_approval_cancel"),
539
+ id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
540
+ targetId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
541
+ toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
542
+ })
543
+ .strict();
544
+ const OmpToolApprovalResponseSchema = z.union([
545
+ z
546
+ .object({
547
+ type: z.literal("tool_approval_response"),
548
+ id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
549
+ toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
550
+ approved: z.boolean(),
551
+ })
552
+ .strict(),
553
+ z
554
+ .object({
555
+ type: z.literal("tool_approval_response"),
556
+ id: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
557
+ toolCallId: boundedString(MAX_TOOL_APPROVAL_ID_BYTES, 1),
558
+ cancelled: z.literal(true),
559
+ timedOut: z.boolean().optional(),
560
+ })
561
+ .strict(),
562
+ ]);
563
+ const OmpAgentEndEnvelopeSchema = z.object({
564
+ type: z.literal("agent_end"),
565
+ messageCount: z.number().int().nonnegative().optional(),
566
+ isTerminal: z.boolean().optional(),
567
+ });
568
+ const OmpCompactionStartSchema = z.object({
569
+ type: z.literal("compaction_start"),
570
+ reason: boundedString(4_096).optional(),
571
+ });
572
+ const OmpCompactionEndSchema = z.object({
573
+ type: z.literal("compaction_end"),
574
+ reason: boundedString(4_096).optional(),
575
+ result: BoundedToolPayloadSchema.optional(),
576
+ aborted: z.boolean().optional(),
577
+ willRetry: z.boolean().optional(),
578
+ errorMessage: boundedString(4_096).optional(),
579
+ skipped: z.boolean().optional(),
580
+ });
581
+ const OmpAgentSessionEventSchema = z.discriminatedUnion("type", [
582
+ z.object({ type: z.literal("agent_start") }),
583
+ z.object({
584
+ type: z.literal("agent_end"),
585
+ messages: z.array(OmpMessageSchema).max(MAX_ARRAY_ITEMS).optional(),
586
+ messageCount: z.number().int().nonnegative().optional(),
587
+ isTerminal: z.boolean().optional(),
588
+ }),
589
+ z.object({ type: z.literal("turn_start") }),
590
+ z.object({ type: z.literal("turn_end") }),
591
+ z.object({ type: z.literal("message_start"), message: OmpMessageSchema }),
592
+ z.object({
593
+ type: z.literal("message_update"),
594
+ message: OmpMessageSchema,
595
+ assistantMessageEvent: OmpAssistantMessageEventSchema.optional(),
596
+ }),
597
+ z.object({ type: z.literal("message_end"), message: OmpMessageSchema }),
598
+ z.object({
599
+ type: z.literal("tool_execution_start"),
600
+ toolCallId: IDENTIFIER,
601
+ toolName: NAME,
602
+ args: BoundedToolPayloadSchema,
603
+ }),
604
+ z.object({
605
+ type: z.literal("tool_execution_update"),
606
+ toolCallId: IDENTIFIER,
607
+ toolName: NAME,
608
+ args: BoundedToolPayloadSchema.optional(),
609
+ partialResult: BoundedToolPayloadSchema,
610
+ }),
611
+ z.object({
612
+ type: z.literal("tool_execution_end"),
613
+ toolCallId: IDENTIFIER,
614
+ toolName: NAME,
615
+ result: BoundedToolPayloadSchema,
616
+ isError: z.boolean().optional(),
617
+ }),
618
+ OmpCompactionStartSchema,
619
+ OmpCompactionEndSchema,
620
+ ]);
621
+ const OmpGoalSchema = z.object({
622
+ id: IDENTIFIER.optional(),
623
+ objective: TEXT.optional(),
624
+ status: boundedString(256).optional(),
625
+ tokenBudget: z.number().finite().nonnegative().optional(),
626
+ tokensUsed: z.number().finite().nonnegative().optional(),
627
+ timeUsedSeconds: z.number().finite().nonnegative().optional(),
628
+ createdAt: boundedString(128).optional(),
629
+ updatedAt: boundedString(128).optional(),
630
+ });
631
+ const OmpGoalModeStateSchema = z.object({
632
+ enabled: z.boolean().optional(),
633
+ mode: boundedString(256).optional(),
634
+ reason: boundedString(4_096).optional(),
635
+ goal: OmpGoalSchema.optional(),
636
+ });
637
+ const OmpSubagentStatusSchema = z.enum([
638
+ "pending",
639
+ "running",
640
+ "started",
641
+ "completed",
642
+ "failed",
643
+ "aborted",
644
+ ]);
645
+ const OmpSubagentLifecyclePayloadSchema = z.object({
646
+ id: IDENTIFIER,
647
+ agent: NAME,
648
+ agentSource: NAME.optional(),
649
+ description: boundedString(64 * 1024).optional(),
650
+ status: OmpSubagentStatusSchema,
651
+ sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
652
+ parentToolCallId: IDENTIFIER.optional(),
653
+ index: z.number().int().nonnegative().max(10_000),
654
+ detached: z.boolean().optional(),
655
+ });
656
+ const OmpSubagentProgressSchema = z.object({
657
+ id: IDENTIFIER,
658
+ status: OmpSubagentStatusSchema,
659
+ description: boundedString(64 * 1024).optional(),
660
+ currentTool: BoundedToolPayloadSchema.optional(),
661
+ recentTools: z.array(BoundedToolPayloadSchema).max(64).optional(),
662
+ recentOutput: z.array(BoundedToolPayloadSchema).max(128).optional(),
663
+ resolvedModel: NAME.optional(),
664
+ });
665
+ const OmpSubagentProgressPayloadSchema = z.object({
666
+ index: z.number().int().nonnegative().max(10_000),
667
+ agent: NAME,
668
+ agentSource: NAME.optional(),
669
+ task: TEXT,
670
+ parentToolCallId: IDENTIFIER.optional(),
671
+ assignment: TEXT.optional(),
672
+ progress: OmpSubagentProgressSchema,
673
+ sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
674
+ detached: z.boolean().optional(),
675
+ });
676
+ const ExtensionUiBase = { type: z.literal("extension_ui_request"), id: IDENTIFIER };
677
+ const OmpExtensionUiRequestSchema = z.discriminatedUnion("method", [
678
+ z
679
+ .object({
680
+ ...ExtensionUiBase,
681
+ method: z.literal("select"),
682
+ title: boundedString(4_096),
683
+ options: z.array(boundedString(4_096)).min(1).max(128),
684
+ optionDetails: z
685
+ .array(z.object({ description: boundedString(16_384).optional() }).strict())
686
+ .max(128)
687
+ .optional(),
688
+ timeout: z.number().nonnegative().finite().optional(),
689
+ })
690
+ .strict()
691
+ .superRefine((request, context) => {
692
+ if (request.optionDetails && request.optionDetails.length !== request.options.length) {
693
+ context.addIssue({ code: "custom", message: "invalid extension UI request" });
694
+ }
695
+ }),
696
+ z
697
+ .object({
698
+ ...ExtensionUiBase,
699
+ method: z.literal("confirm"),
700
+ title: boundedString(4_096),
701
+ message: boundedString(64 * 1024),
702
+ timeout: z.number().nonnegative().finite().optional(),
703
+ })
704
+ .strict(),
705
+ z
706
+ .object({
707
+ ...ExtensionUiBase,
708
+ method: z.literal("input"),
709
+ title: boundedString(4_096),
710
+ placeholder: boundedString(4_096).optional(),
711
+ prefill: TEXT.optional(),
712
+ timeout: z.number().nonnegative().finite().optional(),
713
+ })
714
+ .strict(),
715
+ z
716
+ .object({
717
+ ...ExtensionUiBase,
718
+ method: z.literal("editor"),
719
+ title: boundedString(4_096),
720
+ prefill: TEXT.optional(),
721
+ promptStyle: z.boolean().optional(),
722
+ timeout: z.number().nonnegative().finite().optional(),
723
+ })
724
+ .strict(),
725
+ z.object({ ...ExtensionUiBase, method: z.literal("cancel"), targetId: IDENTIFIER }).strict(),
726
+ z
727
+ .object({
728
+ ...ExtensionUiBase,
729
+ method: z.literal("notify"),
730
+ message: boundedString(64 * 1024),
731
+ notifyType: z.enum(["info", "warning", "error"]).optional(),
732
+ })
733
+ .strict(),
734
+ z
735
+ .object({
736
+ ...ExtensionUiBase,
737
+ method: z.literal("setStatus"),
738
+ statusKey: NAME,
739
+ statusText: boundedString(16_384).optional(),
740
+ })
741
+ .strict(),
742
+ z
743
+ .object({
744
+ ...ExtensionUiBase,
745
+ method: z.literal("setWidget"),
746
+ widgetKey: NAME,
747
+ widgetLines: z.array(boundedString(16_384)).max(128).optional(),
748
+ widgetPlacement: z.enum(["aboveEditor", "belowEditor"]).optional(),
749
+ })
750
+ .strict(),
751
+ z
752
+ .object({ ...ExtensionUiBase, method: z.literal("setTitle"), title: boundedString(4_096) })
753
+ .strict(),
754
+ z.object({ ...ExtensionUiBase, method: z.literal("set_editor_text"), text: TEXT }).strict(),
755
+ z
756
+ .object({
757
+ ...ExtensionUiBase,
758
+ method: z.literal("open_url"),
759
+ url: boundedString(16_384),
760
+ launchUrl: boundedString(16_384).optional(),
761
+ instructions: boundedString(64 * 1024).optional(),
762
+ })
763
+ .strict(),
764
+ ]);
765
+ const OmpRuntimeEventSchema = z.discriminatedUnion("type", [
766
+ ...OmpAgentSessionEventSchema.options,
767
+ z.object({
768
+ type: z.literal("subagent_lifecycle"),
769
+ payload: OmpSubagentLifecyclePayloadSchema,
770
+ }),
771
+ z.object({
772
+ type: z.literal("subagent_progress"),
773
+ payload: OmpSubagentProgressPayloadSchema,
774
+ }),
775
+ z.object({
776
+ type: z.literal("subagent_event"),
777
+ payload: z.object({ id: IDENTIFIER, event: OmpAgentSessionEventSchema }),
778
+ }),
779
+ z.object({
780
+ type: z.literal("todo_reminder"),
781
+ todos: z
782
+ .array(
783
+ z.object({
784
+ id: IDENTIFIER.optional(),
785
+ content: boundedString(16_384),
786
+ status: z.enum(["pending", "in_progress", "blocked", "completed", "abandoned"]),
787
+ }),
788
+ )
789
+ .max(MAX_TODOS),
790
+ }),
791
+ z.object({ type: z.literal("model_changed") }),
792
+ z.object({
793
+ type: z.literal("thinking_level_changed"),
794
+ thinkingLevel: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES).optional(),
795
+ }),
796
+ z.object({
797
+ type: z.literal("goal_updated"),
798
+ goal: OmpGoalSchema.nullable().optional(),
799
+ state: OmpGoalModeStateSchema.optional(),
800
+ }),
801
+ z.object({
802
+ type: z.literal("auto_retry_start"),
803
+ attempt: z.number().int().nonnegative().safe(),
804
+ maxAttempts: z.number().int().positive().safe(),
805
+ delayMs: z.number().int().nonnegative().safe(),
806
+ errorMessage: boundedString(64 * 1024),
807
+ errorId: z.number().int().safe().optional(),
808
+ }),
809
+ z.object({
810
+ type: z.literal("auto_retry_end"),
811
+ success: z.boolean(),
812
+ attempt: z.number().int().nonnegative().safe(),
813
+ finalError: boundedString(64 * 1024).optional(),
814
+ recoveredErrors: BoundedToolPayloadSchema.optional(),
815
+ }),
816
+ z.object({
817
+ type: z.literal("retry_fallback_applied"),
818
+ from: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
819
+ to: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
820
+ role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
821
+ }),
822
+ z.object({
823
+ type: z.literal("retry_fallback_succeeded"),
824
+ model: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
825
+ role: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
826
+ }),
827
+ z.object({ type: z.literal("todo_auto_clear") }),
828
+ z.object({
829
+ type: z.literal("auto_compaction_start"),
830
+ reason: boundedString(4_096),
831
+ action: boundedString(MAX_CONFIG_EVENT_TEXT_BYTES),
832
+ }),
833
+ z.object({
834
+ type: z.literal("auto_compaction_end"),
835
+ action: NAME.optional(),
836
+ result: OmpCompactionResultSchema.nullable().optional(),
837
+ aborted: z.boolean().optional(),
838
+ willRetry: z.boolean().optional(),
839
+ errorMessage: boundedString(64 * 1024).optional(),
840
+ skipped: z.boolean().optional(),
841
+ }),
842
+ z.object({
843
+ type: z.literal("available_commands_update"),
844
+ commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
845
+ }),
846
+ z.object({
847
+ type: z.literal("notice"),
848
+ id: IDENTIFIER.optional(),
849
+ level: z.enum(["info", "warning", "error"]),
850
+ message: boundedString(64 * 1024),
851
+ source: boundedString(MAX_NAME_LENGTH).optional(),
852
+ }),
853
+ z.object({ type: z.literal("command_output"), text: TEXT.optional() }),
854
+ OmpExtensionUiRequestSchema,
855
+ z.object({
856
+ type: z.literal("prompt_result"),
857
+ id: IDENTIFIER.optional(),
858
+ agentInvoked: z.boolean(),
859
+ }),
860
+ OmpHostToolCallSchema,
861
+ OmpHostToolCancelSchema,
862
+ OmpToolApprovalRequestSchema,
863
+ OmpToolApprovalCancelSchema,
864
+ z.object({ type: z.literal("advisor_yielded") }),
865
+ ]);
866
+ const OmpModelsResultSchema = z.object({
867
+ models: z.array(OmpModelSchema).min(1).max(256),
868
+ });
869
+ const OmpPromptAckSchema = z.object({ agentInvoked: z.boolean().optional() }).optional();
870
+ const OmpAvailableCommandsResultSchema = z.object({
871
+ commands: z.array(OmpAvailableCommandSchema).max(MAX_ARRAY_ITEMS),
872
+ });
873
+ const OmpBranchMessagesResultSchema = z.object({
874
+ messages: z.array(z.object({ entryId: IDENTIFIER, text: TEXT })).max(1_024),
875
+ });
876
+ const OmpBranchResultSchema = z.object({ text: TEXT, cancelled: z.boolean() });
877
+ const OmpMessagesResultSchema = z.object({
878
+ messages: z.array(OmpMessageSchema).max(100_000),
879
+ });
880
+ const OmpSubagentsResultSchema = z.object({
881
+ subagents: z
882
+ .array(
883
+ z.object({
884
+ id: IDENTIFIER,
885
+ index: z.number().int().nonnegative().max(10_000),
886
+ agent: NAME,
887
+ agentSource: NAME.optional(),
888
+ description: TEXT.optional(),
889
+ status: OmpSubagentStatusSchema,
890
+ task: TEXT.optional(),
891
+ assignment: TEXT.optional(),
892
+ sessionFile: boundedString(MAX_PATH_LENGTH).optional(),
893
+ lastUpdate: z.number().finite().nonnegative(),
894
+ parentToolCallId: IDENTIFIER.optional(),
895
+ }),
896
+ )
897
+ .max(1_024),
898
+ });
899
+ const OmpSubagentMessagesResultSchema = z.object({
900
+ sessionFile: boundedString(MAX_PATH_LENGTH),
901
+ fromByte: z.number().int().nonnegative(),
902
+ nextByte: z.number().int().nonnegative(),
903
+ reset: z.boolean(),
904
+ messages: z.array(OmpMessageSchema).max(100_000),
905
+ });
906
+ const ProtocolNegotiationResultSchema = z.object({
907
+ protocolVersion: z.literal(2),
908
+ clientCapabilities: z.object({ typedToolApprovals: z.literal(1).optional() }).optional(),
909
+ });
910
+
911
+ export type OmpModel = z.infer<typeof OmpModelSchema>;
912
+ export type OmpSessionState = z.infer<typeof OmpSessionStateSchema>;
913
+ export type OmpSessionStats = z.infer<typeof OmpSessionStatsSchema>;
914
+ export type OmpCompactionResult = z.infer<typeof OmpCompactionResultSchema>;
915
+ export type OmpHostToolDefinition = z.infer<typeof OmpHostToolDefinitionSchema>;
916
+ export type OmpHostToolCall = z.infer<typeof OmpHostToolCallSchema>;
917
+ export type OmpHostToolResult = z.infer<typeof OmpHostToolResultSchema>;
918
+ export type OmpHostToolUpdate = z.infer<typeof OmpHostToolUpdateSchema>;
919
+ export type OmpToolApprovalRequest = z.infer<typeof OmpToolApprovalRequestSchema>;
920
+ export type OmpToolApprovalCancel = z.infer<typeof OmpToolApprovalCancelSchema>;
921
+ export type OmpToolApprovalResponse = z.infer<typeof OmpToolApprovalResponseSchema>;
922
+ export function parseOmpHostToolAgentResult(value: unknown): OmpHostToolResult["result"] {
923
+ return OmpHostToolAgentResultSchema.parse(value);
924
+ }
925
+ export type OmpRpcEvent =
926
+ | z.infer<typeof OmpRuntimeEventSchema>
927
+ | { type: "prompt_error"; id: string; error: string }
928
+ | { type: "process_exit"; error: string };
929
+ export type OmpAgentSessionEvent = z.infer<typeof OmpAgentSessionEventSchema>;
930
+ export type OmpSubagentSnapshot = z.infer<typeof OmpSubagentsResultSchema>["subagents"][number];
931
+ export type OmpSubagentEvent = Extract<
932
+ z.infer<typeof OmpRuntimeEventSchema>,
933
+ { type: "subagent_lifecycle" | "subagent_progress" | "subagent_event" }
934
+ >;
935
+ export interface OmpSubagentMessagesResult {
936
+ sessionFile: string;
937
+ fromByte: number;
938
+ nextByte: number;
939
+ reset: boolean;
940
+ messages: OmpMessage[];
941
+ }
942
+ export interface OmpPersistedSubagentMessages {
943
+ sessionFile: string;
944
+ nativeSessionId: string;
945
+ byteLength: number;
946
+ messages: OmpMessage[];
947
+ }
948
+
949
+ export interface OmpStartOptions {
950
+ cwd: string;
951
+ env?: Readonly<Record<string, string>>;
952
+ outputRedaction?: OmpOutputRedaction;
953
+ inheritEnv?: readonly string[];
954
+ /** Server-owned environment source; tests provide isolated roots instead of ambient process.env. */
955
+ environment?: NodeJS.ProcessEnv;
956
+ command?: readonly string[];
957
+ model?: string;
958
+ mode?: "full" | "write" | "ask";
959
+ thinkingOption?: string;
960
+ systemPrompt?: string;
961
+ roleModels?: Readonly<{ smol?: string; slow?: string; plan?: string }>;
962
+ tools?: readonly string[];
963
+ sessionDir?: string;
964
+ readyTimeoutMs?: number;
965
+ requestTimeoutMs?: number;
966
+ /** Resume this exact native OMP session; never use this to start a new conversation. */
967
+ resumeSessionId?: string;
968
+ noSession?: boolean;
969
+ signal?: AbortSignal;
970
+ }
971
+
972
+ export type OmpAvailableCommand = z.infer<typeof OmpAvailableCommandSchema>;
973
+ export type OmpImage = { type: "image"; data: string; mimeType: string };
974
+ export type OmpExtensionUiResponse =
975
+ | { type: "extension_ui_response"; id: string; value: string }
976
+ | { type: "extension_ui_response"; id: string; confirmed: boolean }
977
+ | { type: "extension_ui_response"; id: string; cancelled: true; timedOut?: boolean };
978
+
979
+ export interface OmpRuntimeSession {
980
+ readonly maxHostToolFrameBytes?: number;
981
+ /** Maximum bytes accepted for one unchunked stdin JSONL frame, including its newline. */
982
+ readonly maxInputFrameBytes?: number;
983
+ readonly supportsTypedToolApprovals: boolean;
984
+ onEvent(listener: (event: OmpRpcEvent) => void): () => void;
985
+ getState(): Promise<OmpSessionState>;
986
+ getSessionStats(): Promise<OmpSessionStats>;
987
+ getAvailableModels(): Promise<OmpModel[]>;
988
+ getAvailableCommands(): Promise<OmpAvailableCommand[]>;
989
+ setSubagentSubscription(level: "events"): Promise<void>;
990
+ readonly inheritedRedactionValues?: readonly string[];
991
+ getSubagents(): Promise<OmpSubagentSnapshot[]>;
992
+ getSubagentMessages(selector: {
993
+ subagentId?: string;
994
+ sessionFile?: string;
995
+ }): Promise<OmpSubagentMessagesResult>;
996
+ prompt(
997
+ message: string,
998
+ images?: readonly OmpImage[],
999
+ onAccepted?: () => void,
1000
+ ): Promise<{ requestId: string; agentInvoked?: boolean }>;
1001
+ compact(customInstructions?: string): Promise<OmpCompactionResult>;
1002
+ setAutoCompaction(enabled: boolean): Promise<void>;
1003
+ setModel(provider: string, modelId: string): Promise<OmpModel>;
1004
+ setThinkingLevel(level: string): Promise<void>;
1005
+ steer(message: string, images?: readonly OmpImage[]): Promise<void>;
1006
+ followUp(message: string, images?: readonly OmpImage[]): Promise<void>;
1007
+ handoff(customInstructions?: string): Promise<void>;
1008
+ respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void>;
1009
+ respondToToolApproval(response: OmpToolApprovalResponse): Promise<void>;
1010
+ getBranchMessages(): Promise<Array<{ entryId: string; text: string }>>;
1011
+ branch(entryId: string): Promise<{ text: string; cancelled: boolean }>;
1012
+ readonly canReplayHistory: boolean;
1013
+ getMessages(): Promise<OmpMessage[]>;
1014
+ abort(): Promise<void>;
1015
+ setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]>;
1016
+ sendHostToolResult(result: OmpHostToolResult): void;
1017
+ sendHostToolUpdate(update: OmpHostToolUpdate): void;
1018
+ close(): Promise<void>;
1019
+ }
1020
+
1021
+ export interface OmpRuntime {
1022
+ readonly supportsPersistence: boolean;
1023
+ startSession(options: OmpStartOptions): Promise<OmpRuntimeSession>;
1024
+ listSessions(options: OmpSessionListOptions): Promise<OmpSessionDescriptor[]>;
1025
+ readPersistedSubagentTranscript(options: {
1026
+ parentSessionFile: string;
1027
+ childTranscriptId: string;
1028
+ cwd: string;
1029
+ signal?: AbortSignal;
1030
+ }): Promise<OmpPersistedSubagentMessages>;
1031
+ }
1032
+
1033
+ export interface OmpSpawnRequest {
1034
+ command: string;
1035
+ args: string[];
1036
+ cwd: string;
1037
+ env: NodeJS.ProcessEnv;
1038
+ detached: boolean;
1039
+ inheritedRedactionValues: string[];
1040
+ }
1041
+
1042
+ export interface OmpRpcRuntimeOptions {
1043
+ spawnProcess?: (request: OmpSpawnRequest) => ChildProcessWithoutNullStreams;
1044
+ terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
1045
+ environment?: NodeJS.ProcessEnv;
1046
+ requestTimeoutMs?: number;
1047
+ listSessions?: (
1048
+ options: OmpSessionListOptions,
1049
+ ) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
1050
+ }
1051
+
1052
+ type PendingRequest = {
1053
+ resolve(value: unknown): void;
1054
+ reject(error: Error): void;
1055
+ timer?: TimerHandle;
1056
+ command: string;
1057
+ beforeResolve?: (value: unknown) => void;
1058
+ };
1059
+ type StartedRequest = { id: string; promise: Promise<unknown> };
1060
+
1061
+ type ReadyFrame = z.infer<typeof OmpReadyFrameSchema>;
1062
+ type ChunkFrame = z.infer<typeof OmpChunkFrameSchema>;
1063
+
1064
+ type ChunkState = {
1065
+ id: string;
1066
+ count: number;
1067
+ byteLength: number;
1068
+ parts: Buffer[];
1069
+ receivedBytes: number;
1070
+ timer: TimerHandle;
1071
+ };
1072
+
1073
+ // The daemon contributes only process/runtime discovery variables plus provider authentication
1074
+ // families. Session-scoped values are explicit host input and are overlaid after rejecting loader
1075
+ // and executable-resolution controls; this keeps provider credentials available without copying
1076
+ const INHERITED_RUNTIME_ENV: Readonly<Record<string, true>> = {
1077
+ ALL_PROXY: true,
1078
+ APPDATA: true,
1079
+ COLORTERM: true,
1080
+ HOME: true,
1081
+ HTTPS_PROXY: true,
1082
+ HTTP_PROXY: true,
1083
+ LANG: true,
1084
+ LC_ALL: true,
1085
+ LC_CTYPE: true,
1086
+ LOCALAPPDATA: true,
1087
+ LOGNAME: true,
1088
+ NO_PROXY: true,
1089
+ OMP_PROFILE: true,
1090
+ PATH: true,
1091
+ PATHEXT: true,
1092
+ PI_CODING_AGENT_DIR: true,
1093
+ PI_CONFIG_DIR: true,
1094
+ PI_PROFILE: true,
1095
+ SHELL: true,
1096
+ SSH_AUTH_SOCK: true,
1097
+ SSL_CERT_DIR: true,
1098
+ SSL_CERT_FILE: true,
1099
+ SYSTEMROOT: true,
1100
+ TEMP: true,
1101
+ TMP: true,
1102
+ TMPDIR: true,
1103
+ TZ: true,
1104
+ USER: true,
1105
+ USERPROFILE: true,
1106
+ XDG_CACHE_HOME: true,
1107
+ XDG_CONFIG_HOME: true,
1108
+ XDG_DATA_HOME: true,
1109
+ XDG_STATE_HOME: true,
1110
+ XDG_RUNTIME_DIR: true,
1111
+ };
1112
+ const INHERITED_PROVIDER_AUTH_ENV: Readonly<Record<string, true>> = {
1113
+ AI_GATEWAY_API_KEY: true,
1114
+ AIAND_API_KEY: true,
1115
+ ALIBABA_CODING_PLAN_API_KEY: true,
1116
+ ALIBABA_TOKEN_PLAN_API_KEY: true,
1117
+ ANTHROPIC_API_KEY: true,
1118
+ ANTHROPIC_FOUNDRY_API_KEY: true,
1119
+ ANTHROPIC_OAUTH_TOKEN: true,
1120
+ AWS_ACCESS_KEY_ID: true,
1121
+ AWS_DEFAULT_REGION: true,
1122
+ AWS_PROFILE: true,
1123
+ AWS_REGION: true,
1124
+ AWS_SECRET_ACCESS_KEY: true,
1125
+ AWS_SESSION_TOKEN: true,
1126
+ AZURE_CLIENT_ID: true,
1127
+ AZURE_CLIENT_SECRET: true,
1128
+ AZURE_OPENAI_API_KEY: true,
1129
+ AZURE_OPENAI_ENDPOINT: true,
1130
+ AZURE_TENANT_ID: true,
1131
+ BAILIAN_TOKEN_PLAN_API_KEY: true,
1132
+ BASETEN_API_KEY: true,
1133
+ CEREBRAS_API_KEY: true,
1134
+ CHARM_HYPER_API_KEY: true,
1135
+ CLINE_API_KEY: true,
1136
+ CLOUDFLARE_AI_GATEWAY_API_KEY: true,
1137
+ COHERE_API_KEY: true,
1138
+ COMMAND_CODE_API_KEY: true,
1139
+ COREWEAVE_API_KEY: true,
1140
+ CURSOR_ACCESS_TOKEN: true,
1141
+ DEEPINFRA_API_KEY: true,
1142
+ DEEPSEEK_API_KEY: true,
1143
+ DEVIN_API_KEY: true,
1144
+ FIREPASS_API_KEY: true,
1145
+ FIREWORKS_API_KEY: true,
1146
+ FUGU_API_KEY: true,
1147
+ GEMINI_API_KEY: true,
1148
+ GMI_API_KEY: true,
1149
+ GOOGLE_API_KEY: true,
1150
+ GOOGLE_APPLICATION_CREDENTIALS: true,
1151
+ GROQ_API_KEY: true,
1152
+ HF_TOKEN: true,
1153
+ HUGGINGFACE_HUB_TOKEN: true,
1154
+ LLAMA_CPP_API_KEY: true,
1155
+ LM_STUDIO_API_KEY: true,
1156
+ META_API_KEY: true,
1157
+ MINIMAX_API_KEY: true,
1158
+ MINIMAX_CODE_API_KEY: true,
1159
+ MINIMAX_CODE_CN_API_KEY: true,
1160
+ MISTRAL_API_KEY: true,
1161
+ MODEL_API_KEY: true,
1162
+ MOONSHOT_API_KEY: true,
1163
+ NANO_GPT_API_KEY: true,
1164
+ NVIDIA_API_KEY: true,
1165
+ NOVITA_API_KEY: true,
1166
+ OLLAMA_API_KEY: true,
1167
+ OLLAMA_CLOUD_API_KEY: true,
1168
+ OLLAMA_HOST: true,
1169
+ OMP_AUTH_BROKER_TOKEN: true,
1170
+ OMP_AUTH_BROKER_URL: true,
1171
+ OPENCODE_API_KEY: true,
1172
+ OPENAI_API_KEY: true,
1173
+ OPENAI_CODEX_OAUTH_TOKEN: true,
1174
+ OPENROUTER_API_KEY: true,
1175
+ PLEXUS_API_KEY: true,
1176
+ QIANFAN_API_KEY: true,
1177
+ QWEN_OAUTH_TOKEN: true,
1178
+ QWEN_PORTAL_API_KEY: true,
1179
+ SAKANA_API_KEY: true,
1180
+ SILICONFLOW_API_KEY: true,
1181
+ SILICONFLOW_CN_API_KEY: true,
1182
+ SYNTHETIC_API_KEY: true,
1183
+ TOGETHER_API_KEY: true,
1184
+ UMANS_AI_CODING_PLAN_API_KEY: true,
1185
+ VENICE_API_KEY: true,
1186
+ VLLM_API_KEY: true,
1187
+ WAFER_SERVERLESS_API_KEY: true,
1188
+ WANDB_API_KEY: true,
1189
+ XAI_API_KEY: true,
1190
+ XAI_OAUTH_TOKEN: true,
1191
+ XIAOMI_API_KEY: true,
1192
+ XIAOMI_TOKEN_PLAN_AMS_API_KEY: true,
1193
+ XIAOMI_TOKEN_PLAN_CN_API_KEY: true,
1194
+ XIAOMI_TOKEN_PLAN_SGP_API_KEY: true,
1195
+ YOLO_AUTO_API_KEY: true,
1196
+ ZAI_API_KEY: true,
1197
+ ZENMUX_API_KEY: true,
1198
+ ZHIPU_API_KEY: true,
1199
+ };
1200
+ const BLOCKED_SESSION_ENV =
1201
+ /^(?:BASH_ENV|BUN_INSTALL.*|BUN_OPTIONS|CLASSPATH|CLAUDE_BASH_NO_CI|CLAUDE_BASH_NO_LOGIN|CLAUDE_CODE_SHELL_PREFIX|DYLD_.*|EDITOR|ELECTRON_RUN_AS_NODE|ENV|GEM_HOME|GEM_PATH|GIT_CONFIG.*|GIT_SSH_COMMAND|HOME|JAVA_TOOL_OPTIONS|LD_.*|NODE_OPTIONS|NODE_PATH|NPM_CONFIG_.*|OMP_AUTORESEARCH_DB_DIR|OMP_COMMAND|OMP_GITHUB_CACHE_DB|OMP_PROFILE|OMP_WORKTREE_DIR|PATH|PATHEXT|PERL5LIB|PERL5OPT|PI_BASH_NO_CI|PI_BASH_NO_LOGIN|PI_CODING_AGENT_DIR|PI_CODING_AGENT_SESSION_DIR|PI_CONFIG_DIR|PI_CONFIG_FILES|PI_GIT_COMMON_DIR|PI_PACKAGE_DIR|PI_PROFILE|PI_PROJECT_DIR|PI_SESSION_ID|PI_SHELL_PREFIX|PI_SUBPROCESS_CMD|PI_WORKTREE_DIR|PWD|PYTHONHOME|PYTHONINSPECT|PYTHONPATH|PYTHONSTARTUP|RUBYLIB|RUBYOPT|SHELL|SYSTEMROOT|USERPROFILE|VISUAL|XDG_CACHE_HOME|XDG_CONFIG_HOME|XDG_DATA_HOME|XDG_RUNTIME_DIR|XDG_STATE_HOME|_JAVA_OPTIONS)$/u;
1202
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
1203
+
1204
+ function validateBoundedText(value: unknown, field: string, maxBytes: number): string {
1205
+ if (
1206
+ typeof value !== "string" ||
1207
+ utf8Bytes(value) === 0 ||
1208
+ utf8Bytes(value) > maxBytes ||
1209
+ value.includes("\0")
1210
+ ) {
1211
+ throw new Error(`Invalid OMP ${field}`);
1212
+ }
1213
+ return value;
1214
+ }
1215
+
1216
+ function inheritedEnvironmentNames(inheritEnv: readonly string[] | undefined): ReadonlySet<string> {
1217
+ if (inheritEnv === undefined) return new Set();
1218
+ if (!Array.isArray(inheritEnv)) {
1219
+ throw new Error("OMP inherited environment names are invalid");
1220
+ }
1221
+ if (inheritEnv.length > MAX_ENV_ENTRIES) {
1222
+ throw new Error("OMP inherited environment has too many entries");
1223
+ }
1224
+ const names = new Set<string>();
1225
+ for (const name of inheritEnv) {
1226
+ if (typeof name !== "string" || !ENV_NAME.test(name)) {
1227
+ throw new Error("OMP inherited environment contains an invalid name");
1228
+ }
1229
+ if (BLOCKED_SESSION_ENV.test(name.toUpperCase())) {
1230
+ throw new Error("OMP inherited environment contains a forbidden variable");
1231
+ }
1232
+ names.add(name);
1233
+ }
1234
+ return names;
1235
+ }
1236
+
1237
+ function buildOmpEnvironment(
1238
+ sessionEnv: Readonly<Record<string, string>> | undefined,
1239
+ inheritEnv: readonly string[] | undefined,
1240
+ sourceEnv: NodeJS.ProcessEnv,
1241
+ ): { env: NodeJS.ProcessEnv; inheritedRedactionValues: string[] } {
1242
+ if (
1243
+ sessionEnv !== undefined &&
1244
+ (sessionEnv === null || typeof sessionEnv !== "object" || Array.isArray(sessionEnv))
1245
+ ) {
1246
+ throw new Error("OMP session environment is invalid");
1247
+ }
1248
+ const explicitlyInherited = inheritedEnvironmentNames(inheritEnv);
1249
+ const explicitlyInheritedNormalized = new Set(
1250
+ [...explicitlyInherited].map((name) => name.toUpperCase()),
1251
+ );
1252
+ const explicitNames = new Set(Object.keys(sessionEnv ?? {}).map((name) => name.toUpperCase()));
1253
+ const env: NodeJS.ProcessEnv = {};
1254
+ const inheritedRedactionValues: string[] = [];
1255
+ let totalBytes = 0;
1256
+ for (const [name, value] of Object.entries(sourceEnv)) {
1257
+ if (value === undefined || name.toUpperCase() === "OMP_COMMAND") continue;
1258
+ const normalizedName = name.toUpperCase();
1259
+ const isRuntime =
1260
+ process.platform === "win32"
1261
+ ? normalizedName in INHERITED_RUNTIME_ENV
1262
+ : name in INHERITED_RUNTIME_ENV;
1263
+ const isProviderAuth =
1264
+ process.platform === "win32"
1265
+ ? normalizedName in INHERITED_PROVIDER_AUTH_ENV
1266
+ : name in INHERITED_PROVIDER_AUTH_ENV;
1267
+ const isExplicitlyInherited =
1268
+ process.platform === "win32"
1269
+ ? explicitlyInheritedNormalized.has(normalizedName)
1270
+ : explicitlyInherited.has(name);
1271
+ if (!isRuntime && !isProviderAuth && !isExplicitlyInherited) continue;
1272
+ if (explicitNames.has(normalizedName)) continue;
1273
+ const valueBytes = utf8Bytes(value);
1274
+ if (!ENV_NAME.test(name) || valueBytes > MAX_ENV_VALUE_LENGTH || value.includes("\0")) {
1275
+ if (isExplicitlyInherited) {
1276
+ throw new Error("OMP inherited environment contains an invalid value");
1277
+ }
1278
+ continue;
1279
+ }
1280
+ if (isExplicitlyInherited && valueBytes > 0 && valueBytes < 4) {
1281
+ throw new OmpPublicError("OMP inherited environment value is too short for safe redaction");
1282
+ }
1283
+ totalBytes += utf8Bytes(name) + valueBytes;
1284
+ if (totalBytes > MAX_ENV_TOTAL_LENGTH) {
1285
+ throw new Error("OMP inherited environment is too large");
1286
+ }
1287
+ env[name] = value;
1288
+ if (isExplicitlyInherited && valueBytes > 0) inheritedRedactionValues.push(value);
1289
+ }
1290
+ let entryCount = 0;
1291
+ for (const name in sessionEnv ?? {}) {
1292
+ if (!Object.hasOwn(sessionEnv ?? {}, name)) continue;
1293
+ entryCount += 1;
1294
+ if (entryCount > MAX_ENV_ENTRIES)
1295
+ throw new Error("OMP session environment has too many entries");
1296
+ const value = (sessionEnv as Readonly<Record<string, string>>)[name];
1297
+ const normalizedName = name.toUpperCase();
1298
+ if (!ENV_NAME.test(name) || BLOCKED_SESSION_ENV.test(normalizedName)) {
1299
+ throw new Error("OMP session environment contains a forbidden variable");
1300
+ }
1301
+ if (
1302
+ typeof value !== "string" ||
1303
+ utf8Bytes(value) > MAX_ENV_VALUE_LENGTH ||
1304
+ value.includes("\0")
1305
+ ) {
1306
+ throw new Error("OMP session environment contains an invalid value");
1307
+ }
1308
+ const valueBytes = utf8Bytes(value);
1309
+ totalBytes += utf8Bytes(name) + valueBytes;
1310
+ if (totalBytes > MAX_ENV_TOTAL_LENGTH) throw new Error("OMP session environment is too large");
1311
+ for (const inheritedName of Object.keys(env)) {
1312
+ if (inheritedName !== name && inheritedName.toUpperCase() === normalizedName) {
1313
+ delete env[inheritedName];
1314
+ }
1315
+ }
1316
+ env[name] = value;
1317
+ }
1318
+ return { env, inheritedRedactionValues };
1319
+ }
1320
+
1321
+ export function buildOmpSpawnRequest(
1322
+ options: OmpStartOptions,
1323
+ sourceEnv: NodeJS.ProcessEnv = process.env,
1324
+ ): OmpSpawnRequest {
1325
+ const environmentSource = options.environment ?? sourceEnv;
1326
+ const cwd = validateBoundedText(options.cwd, "working directory", MAX_PATH_LENGTH);
1327
+ if (!isAbsolute(cwd)) throw new Error("OMP working directory must be absolute");
1328
+ const commandPrefix = options.command ?? [environmentSource.OMP_COMMAND ?? "omp"];
1329
+ if (commandPrefix.length === 0) throw new Error("Invalid OMP command");
1330
+ const [rawCommand, ...rawPrefixArgs] = commandPrefix;
1331
+ const command = validateBoundedText(rawCommand, "command", MAX_PATH_LENGTH);
1332
+ const args = rawPrefixArgs.map((argument) =>
1333
+ validateBoundedText(argument, "command argument", MAX_PATH_LENGTH),
1334
+ );
1335
+ if (/[\r\n]/u.test(command)) throw new Error("Invalid OMP command");
1336
+ const mode = options.mode ?? "full";
1337
+ if (mode !== "full" && mode !== "write" && mode !== "ask") throw new Error("Invalid OMP mode");
1338
+ const approvalMode = mode === "full" ? "yolo" : mode === "write" ? "write" : "always-ask";
1339
+ if (!args.some((argument) => argument === "--mode" || argument.startsWith("--mode="))) {
1340
+ args.push("--mode", "rpc-ui");
1341
+ }
1342
+ args.push("--approval-mode", approvalMode);
1343
+ if (options.tools) {
1344
+ if (options.tools.length === 0) args.push("--no-tools");
1345
+ else args.push("--tools", options.tools.join(","));
1346
+ }
1347
+ if (options.model !== undefined) {
1348
+ args.push("--model", validateBoundedText(options.model, "model", MAX_MODEL_SELECTOR_BYTES));
1349
+ }
1350
+ if (options.thinkingOption !== undefined) {
1351
+ const thinking = OmpThinkingLevelSchema.safeParse(options.thinkingOption);
1352
+ if (!thinking.success) throw new Error("Invalid OMP thinking option");
1353
+ args.push("--thinking", thinking.data);
1354
+ }
1355
+ if (options.roleModels?.smol) {
1356
+ args.push(
1357
+ "--smol",
1358
+ validateBoundedText(options.roleModels.smol, "smol model", MAX_MODEL_SELECTOR_BYTES),
1359
+ );
1360
+ }
1361
+ if (options.roleModels?.slow) {
1362
+ args.push(
1363
+ "--slow",
1364
+ validateBoundedText(options.roleModels.slow, "slow model", MAX_MODEL_SELECTOR_BYTES),
1365
+ );
1366
+ }
1367
+ if (options.roleModels?.plan) {
1368
+ args.push(
1369
+ "--plan",
1370
+ validateBoundedText(options.roleModels.plan, "plan model", MAX_MODEL_SELECTOR_BYTES),
1371
+ );
1372
+ }
1373
+ if (options.sessionDir !== undefined) {
1374
+ args.push(
1375
+ "--session-dir",
1376
+ validateBoundedText(options.sessionDir, "session directory", MAX_PATH_LENGTH),
1377
+ );
1378
+ }
1379
+ if (options.resumeSessionId !== undefined) {
1380
+ args.push("--resume", validateNativeSessionId(options.resumeSessionId));
1381
+ }
1382
+ if (options.noSession) args.push("--no-session");
1383
+ const systemPrompt = options.systemPrompt?.trim();
1384
+ if (systemPrompt) {
1385
+ args.push(
1386
+ "--append-system-prompt",
1387
+ validateBoundedText(systemPrompt, "system prompt", MAX_SYSTEM_PROMPT_LENGTH),
1388
+ );
1389
+ }
1390
+ const { env, inheritedRedactionValues } = buildOmpEnvironment(
1391
+ options.env,
1392
+ options.inheritEnv,
1393
+ environmentSource,
1394
+ );
1395
+ env.OMP_NO_WEBP = "1";
1396
+ return {
1397
+ command,
1398
+ args,
1399
+ cwd,
1400
+ env,
1401
+ inheritedRedactionValues,
1402
+ detached: process.platform !== "win32",
1403
+ };
1404
+ }
1405
+
1406
+ function waitWithTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
1407
+ const result = Promise.withResolvers<T>();
1408
+ const timer = setTimeout(() => result.reject(new Error(message)), timeoutMs);
1409
+ promise.then(
1410
+ (value) => {
1411
+ clearTimeout(timer);
1412
+ result.resolve(value);
1413
+ },
1414
+ (error) => {
1415
+ clearTimeout(timer);
1416
+ result.reject(error);
1417
+ },
1418
+ );
1419
+ return result.promise;
1420
+ }
1421
+
1422
+ function waitMs(ms: number): Promise<void> {
1423
+ const result = Promise.withResolvers<void>();
1424
+ setTimeout(result.resolve, ms);
1425
+ return result.promise;
1426
+ }
1427
+
1428
+ function processIsGone(error: unknown): boolean {
1429
+ return (error as NodeJS.ErrnoException)?.code === "ESRCH";
1430
+ }
1431
+
1432
+ function isConfirmedNoProcessSpawnFailure(error: unknown): boolean {
1433
+ const code = (error as NodeJS.ErrnoException)?.code;
1434
+ return code === "ENOENT" || code === "EACCES" || code === "EPERM";
1435
+ }
1436
+
1437
+ /**
1438
+ * Terminates the detached process group created for OMP. This covers descendants that remain in
1439
+ * that group after the leader exits; descendants that deliberately re-parent into another process
1440
+ * group are outside this transport's containment boundary.
1441
+ */
1442
+ export async function terminatePosixProcessTree(
1443
+ pid: number,
1444
+ graceMs: number,
1445
+ signalProcess: (pid: number, signal: NodeJS.Signals | 0) => void = process.kill,
1446
+ wait: (ms: number) => Promise<void> = waitMs,
1447
+ ): Promise<boolean> {
1448
+ try {
1449
+ signalProcess(-pid, 0);
1450
+ } catch (error) {
1451
+ return processIsGone(error);
1452
+ }
1453
+ try {
1454
+ signalProcess(-pid, "SIGTERM");
1455
+ } catch (error) {
1456
+ if (!processIsGone(error)) return false;
1457
+ }
1458
+ await wait(graceMs);
1459
+ try {
1460
+ signalProcess(-pid, 0);
1461
+ } catch (error) {
1462
+ return processIsGone(error);
1463
+ }
1464
+ try {
1465
+ signalProcess(-pid, "SIGKILL");
1466
+ } catch (error) {
1467
+ if (!processIsGone(error)) return false;
1468
+ }
1469
+ await wait(graceMs);
1470
+ try {
1471
+ signalProcess(-pid, 0);
1472
+ return false;
1473
+ } catch (error) {
1474
+ return processIsGone(error);
1475
+ }
1476
+ }
1477
+
1478
+ type ProcessTreeCleanup = "verified" | "uncertain" | "failed";
1479
+
1480
+ async function stopWindowsTree(pid: number): Promise<ProcessTreeCleanup> {
1481
+ const result = Promise.withResolvers<ProcessTreeCleanup>();
1482
+ const systemRoot = process.env.SystemRoot ?? WINDOWS_DEFAULT_SYSTEM_ROOT;
1483
+ let taskkill: ChildProcessWithoutNullStreams;
1484
+ try {
1485
+ taskkill = spawn(
1486
+ join(systemRoot, "System32", "taskkill.exe"),
1487
+ ["/PID", String(pid), "/T", "/F"],
1488
+ {
1489
+ stdio: ["pipe", "pipe", "pipe"],
1490
+ windowsHide: true,
1491
+ env: { SystemRoot: systemRoot },
1492
+ },
1493
+ );
1494
+ } catch {
1495
+ return "failed";
1496
+ }
1497
+ taskkill.stdout.resume();
1498
+ taskkill.stderr.resume();
1499
+ let settled = false;
1500
+ let deadline: TimerHandle | undefined;
1501
+ let finalDeadline: TimerHandle | undefined;
1502
+ const finish = (outcome: ProcessTreeCleanup) => {
1503
+ if (settled) return;
1504
+ settled = true;
1505
+ clearTimeout(deadline);
1506
+ clearTimeout(finalDeadline);
1507
+ result.resolve(outcome);
1508
+ };
1509
+ deadline = setTimeout(() => {
1510
+ taskkill.kill("SIGKILL");
1511
+ finalDeadline = setTimeout(() => finish("failed"), PROCESS_STOP_TIMEOUT_MS);
1512
+ }, PROCESS_STOP_TIMEOUT_MS);
1513
+ taskkill.once("error", () => finish("failed"));
1514
+ taskkill.once("close", (code, signal) => {
1515
+ finish(
1516
+ code === 0 && signal === null
1517
+ ? "verified"
1518
+ : code === 128 && signal === null
1519
+ ? "uncertain"
1520
+ : "failed",
1521
+ );
1522
+ });
1523
+ return result.promise;
1524
+ }
1525
+
1526
+ export async function terminateSpawnedProcessTree(
1527
+ pid: number,
1528
+ platform: NodeJS.Platform = process.platform,
1529
+ ): Promise<boolean> {
1530
+ if (platform === "win32") return (await stopWindowsTree(pid)) === "verified";
1531
+ return await terminatePosixProcessTree(pid, PROCESS_STOP_TIMEOUT_MS);
1532
+ }
1533
+
1534
+ class OmpRpcProcess {
1535
+ readonly ready: Promise<ReadyFrame>;
1536
+ readonly inheritedRedactionValues: readonly string[];
1537
+
1538
+ private readonly child: ChildProcessWithoutNullStreams;
1539
+ private readonly listeners = new Set<(event: OmpRpcEvent) => void>();
1540
+ private readonly pending = new Map<string, PendingRequest>();
1541
+ private readonly queuedWrites = new Map<string, number>();
1542
+ private readonly pendingOneWayWrites = new Map<
1543
+ string,
1544
+ { reject(error: Error): void; timer: TimerHandle }
1545
+ >();
1546
+ private readonly acceptedPromptIds = new Set<string>();
1547
+ private readonly exitPromise: Promise<void>;
1548
+ private readonly resolveExit: () => void;
1549
+ private readonly resolveReady: (frame: ReadyFrame) => void;
1550
+ private readonly rejectReady: (error: Error) => void;
1551
+ private readonly terminateProcessTree: (pid: number) => Promise<ProcessTreeCleanup>;
1552
+ private readonly streamedBlocks = new Map<number, string>();
1553
+ private readonly activeToolCallIds = new Set<string>();
1554
+ private pendingWriteBytes = 0;
1555
+ private commandTextLength = 0;
1556
+ private lineParts: Buffer[] = [];
1557
+ private lineBytes = 0;
1558
+ private discardingLine = false;
1559
+ private discardedLineBytes = 0;
1560
+ private chunk: ChunkState | null = null;
1561
+ private physicalFrameLimit = MAX_PHYSICAL_FRAME_BYTES;
1562
+ private reassembledFrameLimit = MAX_REASSEMBLED_FRAME_BYTES;
1563
+ private closed = false;
1564
+ private exited = false;
1565
+ private fatalError: Error | null = null;
1566
+ private closePromise: Promise<void> | null = null;
1567
+ private treeCleanupPromise: Promise<ProcessTreeCleanup> | null = null;
1568
+ private spawnFailedWithoutProcess = false;
1569
+ private readyReceived = false;
1570
+ private outputSettled = false;
1571
+
1572
+ constructor(
1573
+ options: OmpStartOptions,
1574
+ spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
1575
+ terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
1576
+ private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
1577
+ ) {
1578
+ const ready = Promise.withResolvers<ReadyFrame>();
1579
+ this.rejectReady = ready.reject;
1580
+ this.ready = ready.promise;
1581
+ this.resolveReady = ready.resolve;
1582
+ const request = buildOmpSpawnRequest(options);
1583
+ this.inheritedRedactionValues = request.inheritedRedactionValues;
1584
+ this.terminateProcessTree = terminateProcessTree
1585
+ ? async (pid) => {
1586
+ const outcome = await terminateProcessTree(pid);
1587
+ return outcome === true ? "verified" : outcome === "uncertain" ? "uncertain" : "failed";
1588
+ }
1589
+ : async (pid) =>
1590
+ process.platform === "win32"
1591
+ ? stopWindowsTree(pid)
1592
+ : (await terminatePosixProcessTree(pid, PROCESS_STOP_TIMEOUT_MS))
1593
+ ? "verified"
1594
+ : "failed";
1595
+ try {
1596
+ this.child = spawnProcess
1597
+ ? spawnProcess(request)
1598
+ : spawn(request.command, request.args, {
1599
+ cwd: request.cwd,
1600
+ env: request.env,
1601
+ detached: request.detached,
1602
+ windowsHide: true,
1603
+ stdio: ["pipe", "pipe", "pipe"],
1604
+ });
1605
+ } catch (cause) {
1606
+ const code = (cause as NodeJS.ErrnoException)?.code;
1607
+ throw new Error(
1608
+ code === "ENOENT"
1609
+ ? "OMP executable was not found"
1610
+ : code === "EACCES" || code === "EPERM"
1611
+ ? "OMP executable is not runnable"
1612
+ : "OMP process could not be launched",
1613
+ );
1614
+ }
1615
+ this.child.stdout.on("data", (chunk: Buffer | string) => this.receiveData(chunk));
1616
+ this.child.stdout.once("end", () => this.handleStdoutEnd());
1617
+ this.child.stderr.on("data", () => {
1618
+ // Stderr is intentionally drained and discarded. It may contain credentials or paths.
1619
+ });
1620
+ this.child.stdin.on("error", () => {
1621
+ this.fail(new Error("OMP RPC input channel failed"));
1622
+ });
1623
+ const exited = Promise.withResolvers<void>();
1624
+ this.exitPromise = exited.promise;
1625
+ this.resolveExit = exited.resolve;
1626
+ this.child.once("exit", (code, signal) => this.handleProcessExit(code, signal));
1627
+ this.child.once("close", () => this.settleOutput());
1628
+ this.child.once("error", (cause) => {
1629
+ const code = (cause as NodeJS.ErrnoException)?.code;
1630
+ if (this.child.pid === undefined && isConfirmedNoProcessSpawnFailure(cause)) {
1631
+ this.spawnFailedWithoutProcess = true;
1632
+ }
1633
+ this.fail(
1634
+ new Error(
1635
+ code === "ENOENT"
1636
+ ? "OMP executable was not found"
1637
+ : code === "EACCES" || code === "EPERM"
1638
+ ? "OMP executable is not runnable"
1639
+ : "OMP process could not be launched",
1640
+ ),
1641
+ );
1642
+ });
1643
+ }
1644
+
1645
+ private handleProcessExit(code: number | null, signal: NodeJS.Signals | null): void {
1646
+ if (this.exited) return;
1647
+ this.exited = true;
1648
+ void this.startTreeCleanup();
1649
+ const detail = code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`;
1650
+ const error = new Error(`OMP RPC process exited (${detail})`);
1651
+ this.rejectReady(error);
1652
+ this.failPending(error);
1653
+ this.resolveExit();
1654
+ if (!this.closed && !this.fatalError) this.fail(error);
1655
+ }
1656
+
1657
+ private handleStdoutEnd(): void {
1658
+ this.settleOutput();
1659
+ if (!this.exited && !this.closed) this.fail(new Error("OMP RPC output channel closed"));
1660
+ }
1661
+
1662
+ private settleOutput(): void {
1663
+ if (this.outputSettled) return;
1664
+ this.outputSettled = true;
1665
+ if (this.lineBytes > 0 || this.discardingLine) this.recordProtocolViolation();
1666
+ this.lineParts = [];
1667
+ this.lineBytes = 0;
1668
+ this.discardingLine = false;
1669
+ this.discardedLineBytes = 0;
1670
+ }
1671
+
1672
+ get outboundFrameLimit(): number {
1673
+ return this.physicalFrameLimit;
1674
+ }
1675
+
1676
+ onEvent(listener: (event: OmpRpcEvent) => void): () => void {
1677
+ this.listeners.add(listener);
1678
+ return () => this.listeners.delete(listener);
1679
+ }
1680
+
1681
+ applyReadyLimits(frame: ReadyFrame): void {
1682
+ if (frame.maxFrameBytes !== undefined) this.physicalFrameLimit = frame.maxFrameBytes;
1683
+ if (frame.maxReassembledFrameBytes !== undefined) {
1684
+ this.reassembledFrameLimit = frame.maxReassembledFrameBytes;
1685
+ }
1686
+ }
1687
+
1688
+ startRequest(
1689
+ command: Record<string, unknown>,
1690
+ timeoutMs: number | null = this.requestTimeoutMs,
1691
+ beforeResolve?: (value: unknown) => void,
1692
+ ): StartedRequest {
1693
+ const id = randomUUID();
1694
+ if (this.fatalError) return { id, promise: Promise.reject(this.fatalError) };
1695
+ if (this.closed || this.exited || !this.child.stdin.writable) {
1696
+ return { id, promise: Promise.reject(new Error("OMP RPC process is closed")) };
1697
+ }
1698
+ let payload: Buffer;
1699
+ try {
1700
+ payload = Buffer.from(`${JSON.stringify({ ...command, id })}\n`);
1701
+ } catch {
1702
+ return { id, promise: Promise.reject(new Error("OMP RPC request could not be encoded")) };
1703
+ }
1704
+ if (payload.byteLength > this.physicalFrameLimit) {
1705
+ return {
1706
+ id,
1707
+ promise: Promise.reject(new Error("OMP RPC request exceeds the negotiated frame limit")),
1708
+ };
1709
+ }
1710
+ if (
1711
+ this.pending.size >= MAX_PENDING_REQUESTS ||
1712
+ this.pendingWriteBytes + payload.byteLength > MAX_PENDING_WRITE_BYTES
1713
+ ) {
1714
+ return { id, promise: Promise.reject(new Error("OMP RPC has too many pending requests")) };
1715
+ }
1716
+ const result = Promise.withResolvers<unknown>();
1717
+ const timer =
1718
+ timeoutMs === null
1719
+ ? undefined
1720
+ : setTimeout(() => {
1721
+ this.pending.delete(id);
1722
+ result.reject(new Error("OMP RPC request timed out"));
1723
+ }, timeoutMs);
1724
+ this.pending.set(id, {
1725
+ resolve: result.resolve,
1726
+ reject: result.reject,
1727
+ timer,
1728
+ command: typeof command.type === "string" ? command.type : "unknown",
1729
+ ...(beforeResolve ? { beforeResolve } : {}),
1730
+ });
1731
+ this.queuedWrites.set(id, payload.byteLength);
1732
+ this.pendingWriteBytes += payload.byteLength;
1733
+ try {
1734
+ this.child.stdin.write(payload, (cause) => {
1735
+ this.releaseQueuedWrite(id);
1736
+ if (cause) this.fail(new Error("OMP RPC input channel failed"));
1737
+ });
1738
+ } catch {
1739
+ this.releaseQueuedWrite(id);
1740
+ this.fail(new Error("OMP RPC input channel failed"));
1741
+ }
1742
+ return { id, promise: result.promise };
1743
+ }
1744
+
1745
+ request(
1746
+ command: Record<string, unknown>,
1747
+ timeoutMs: number | null = this.requestTimeoutMs,
1748
+ ): Promise<unknown> {
1749
+ return this.startRequest(command, timeoutMs).promise;
1750
+ }
1751
+
1752
+ send(frame: OmpHostToolResult | OmpHostToolUpdate): void {
1753
+ if (this.fatalError) throw this.fatalError;
1754
+ if (this.closed || this.exited || !this.child.stdin.writable) {
1755
+ throw new Error("OMP RPC process is closed");
1756
+ }
1757
+ const parsed =
1758
+ frame.type === "host_tool_result"
1759
+ ? OmpHostToolResultSchema.parse(frame)
1760
+ : OmpHostToolUpdateSchema.parse(frame);
1761
+ const payload = Buffer.from(`${JSON.stringify(parsed)}\n`);
1762
+ if (payload.byteLength > this.physicalFrameLimit) {
1763
+ throw new Error("OMP host tool frame exceeds the negotiated frame limit");
1764
+ }
1765
+ if (this.pendingWriteBytes + payload.byteLength > MAX_PENDING_WRITE_BYTES) {
1766
+ throw new Error("OMP RPC has too many pending writes");
1767
+ }
1768
+ const writeId = randomUUID();
1769
+ this.queuedWrites.set(writeId, payload.byteLength);
1770
+ this.pendingWriteBytes += payload.byteLength;
1771
+ try {
1772
+ this.child.stdin.write(payload, (cause) => {
1773
+ this.releaseQueuedWrite(writeId);
1774
+ if (cause) this.fail(new Error("OMP RPC input channel failed"));
1775
+ });
1776
+ } catch {
1777
+ this.releaseQueuedWrite(writeId);
1778
+ this.fail(new Error("OMP RPC input channel failed"));
1779
+ throw new Error("OMP RPC input channel failed");
1780
+ }
1781
+ }
1782
+
1783
+ sendFrame(frame: Record<string, unknown>, timeoutMs = this.requestTimeoutMs): Promise<void> {
1784
+ if (this.fatalError) return Promise.reject(this.fatalError);
1785
+ if (this.closed || this.exited || !this.child.stdin.writable) {
1786
+ return Promise.reject(new Error("OMP RPC process is closed"));
1787
+ }
1788
+ let payload: Buffer;
1789
+ try {
1790
+ payload = Buffer.from(`${JSON.stringify(frame)}\n`);
1791
+ } catch {
1792
+ return Promise.reject(new Error("OMP RPC frame could not be encoded"));
1793
+ }
1794
+ if (payload.byteLength > this.physicalFrameLimit) {
1795
+ return Promise.reject(new Error("OMP RPC frame exceeds the negotiated frame limit"));
1796
+ }
1797
+ if (
1798
+ this.pendingOneWayWrites.size >= MAX_PENDING_ONE_WAY_WRITES ||
1799
+ this.pendingWriteBytes + payload.byteLength > MAX_PENDING_WRITE_BYTES
1800
+ ) {
1801
+ return Promise.reject(new Error("OMP RPC has too many pending writes"));
1802
+ }
1803
+ const token = randomUUID();
1804
+ const written = Promise.withResolvers<void>();
1805
+ const timer = setTimeout(() => {
1806
+ if (!this.pendingOneWayWrites.delete(token)) return;
1807
+ this.releaseQueuedWrite(token);
1808
+ const error = new Error("OMP RPC write timed out");
1809
+ written.reject(error);
1810
+ this.fail(error);
1811
+ }, timeoutMs);
1812
+ this.pendingOneWayWrites.set(token, { reject: written.reject, timer });
1813
+ this.queuedWrites.set(token, payload.byteLength);
1814
+ this.pendingWriteBytes += payload.byteLength;
1815
+ try {
1816
+ this.child.stdin.write(payload, (cause) => {
1817
+ const pending = this.pendingOneWayWrites.get(token);
1818
+ if (!pending) return;
1819
+ clearTimeout(pending.timer);
1820
+ this.pendingOneWayWrites.delete(token);
1821
+ this.releaseQueuedWrite(token);
1822
+ if (cause) {
1823
+ const error = new Error("OMP RPC input channel failed");
1824
+ written.reject(error);
1825
+ this.fail(error);
1826
+ } else {
1827
+ written.resolve();
1828
+ }
1829
+ });
1830
+ } catch {
1831
+ clearTimeout(timer);
1832
+ this.pendingOneWayWrites.delete(token);
1833
+ this.releaseQueuedWrite(token);
1834
+ const error = new Error("OMP RPC input channel failed");
1835
+ written.reject(error);
1836
+ this.fail(error);
1837
+ }
1838
+ return written.promise;
1839
+ }
1840
+
1841
+ close(): Promise<void> {
1842
+ this.closePromise ??= this.closeProcess();
1843
+ return this.closePromise;
1844
+ }
1845
+
1846
+ private async closeProcess(): Promise<void> {
1847
+ this.closed = true;
1848
+ this.clearChunk();
1849
+ this.failPending(new Error("OMP RPC process was closed"));
1850
+ if (!this.exited) {
1851
+ try {
1852
+ this.child.stdin.end();
1853
+ } catch {
1854
+ // Continue to process-tree cleanup when the input channel is already closed.
1855
+ }
1856
+ }
1857
+ const cleanupPromise = this.startTreeCleanup();
1858
+ const cleanup = await cleanupPromise;
1859
+ if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed");
1860
+ if (
1861
+ !this.spawnFailedWithoutProcess &&
1862
+ !this.exited &&
1863
+ !(await this.waitForExit(PROCESS_STOP_TIMEOUT_MS))
1864
+ ) {
1865
+ throw new Error("OMP RPC process did not close after tree cleanup");
1866
+ }
1867
+ }
1868
+
1869
+ private startTreeCleanup(): Promise<ProcessTreeCleanup> {
1870
+ if (this.treeCleanupPromise) return this.treeCleanupPromise;
1871
+ const pid = this.child.pid;
1872
+ this.treeCleanupPromise = (
1873
+ pid === undefined
1874
+ ? Promise.resolve<ProcessTreeCleanup>(
1875
+ this.spawnFailedWithoutProcess ? "verified" : "uncertain",
1876
+ )
1877
+ : this.terminateProcessTree(pid)
1878
+ ).catch(() => "failed");
1879
+ return this.treeCleanupPromise;
1880
+ }
1881
+
1882
+ private async waitForExit(timeoutMs: number): Promise<boolean> {
1883
+ const timeout = Promise.withResolvers<false>();
1884
+ const timer = setTimeout(() => timeout.resolve(false), timeoutMs);
1885
+ try {
1886
+ return await Promise.race([this.exitPromise.then(() => true), timeout.promise]);
1887
+ } finally {
1888
+ clearTimeout(timer);
1889
+ }
1890
+ }
1891
+
1892
+ private receiveData(chunk: Buffer | string): void {
1893
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1894
+ let start = 0;
1895
+ for (let index = 0; index < bytes.length; index += 1) {
1896
+ if (bytes[index] !== 10) continue;
1897
+ const part = bytes.subarray(start, index);
1898
+ if (this.discardingLine) {
1899
+ this.discardedLineBytes += part.byteLength;
1900
+ if (this.discardedLineBytes > MAX_SEMANTIC_FRAME_BYTES) {
1901
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
1902
+ return;
1903
+ }
1904
+ this.resetDiscardedLine();
1905
+ } else {
1906
+ this.appendLinePart(part);
1907
+ if (this.fatalError) return;
1908
+ if (this.discardingLine) this.resetDiscardedLine();
1909
+ else this.completeLine();
1910
+ }
1911
+ start = index + 1;
1912
+ }
1913
+ if (start >= bytes.length) return;
1914
+ const trailing = bytes.subarray(start);
1915
+ if (this.discardingLine) {
1916
+ this.discardedLineBytes += trailing.byteLength;
1917
+ if (this.discardedLineBytes > MAX_SEMANTIC_FRAME_BYTES) {
1918
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
1919
+ }
1920
+ } else {
1921
+ this.appendLinePart(trailing);
1922
+ }
1923
+ }
1924
+
1925
+ private resetDiscardedLine(): void {
1926
+ this.discardingLine = false;
1927
+ this.discardedLineBytes = 0;
1928
+ this.lineParts = [];
1929
+ this.lineBytes = 0;
1930
+ }
1931
+
1932
+ private appendLinePart(part: Buffer): void {
1933
+ if (part.byteLength === 0) return;
1934
+ const nextBytes = this.lineBytes + part.byteLength;
1935
+ if (nextBytes > MAX_SEMANTIC_FRAME_BYTES) {
1936
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
1937
+ return;
1938
+ }
1939
+ if (this.lineParts.length >= MAX_LINE_PARTS || nextBytes > this.physicalFrameLimit) {
1940
+ this.discardedLineBytes = nextBytes;
1941
+ this.lineParts = [];
1942
+ this.lineBytes = 0;
1943
+ this.discardingLine = true;
1944
+ this.recordProtocolViolation();
1945
+ return;
1946
+ }
1947
+ this.lineParts.push(part);
1948
+ this.lineBytes = nextBytes;
1949
+ }
1950
+
1951
+ private completeLine(): void {
1952
+ if (this.lineBytes === 0) return;
1953
+ const line = Buffer.concat(this.lineParts, this.lineBytes);
1954
+ this.lineParts = [];
1955
+ this.lineBytes = 0;
1956
+ const payload = line.at(-1) === 13 ? line.subarray(0, -1) : line;
1957
+ if (payload.byteLength > MAX_SEMANTIC_FRAME_BYTES) {
1958
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
1959
+ return;
1960
+ }
1961
+ let decoded: unknown;
1962
+ try {
1963
+ decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
1964
+ } catch {
1965
+ this.recordProtocolViolation();
1966
+ return;
1967
+ }
1968
+ if (this.receiveKnownResponse(decoded)) return;
1969
+ if (this.receiveDegradedAgentEnd(decoded, true)) return;
1970
+ if (
1971
+ boundedJsonBytes(decoded, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
1972
+ Number.POSITIVE_INFINITY
1973
+ ) {
1974
+ this.recordProtocolViolation();
1975
+ return;
1976
+ }
1977
+ const frame = JsonObjectSchema.safeParse(decoded);
1978
+ if (!frame.success) {
1979
+ this.recordProtocolViolation();
1980
+ return;
1981
+ }
1982
+ this.receiveFrame(frame.data);
1983
+ }
1984
+
1985
+ private receiveChunk(frame: ChunkFrame): void {
1986
+ if (
1987
+ frame.byteLength > MAX_SEMANTIC_FRAME_BYTES &&
1988
+ ![...this.pending.values()].some(
1989
+ (pending) =>
1990
+ pending.command === "get_messages" || pending.command === "get_subagent_messages",
1991
+ )
1992
+ ) {
1993
+ this.fail(new Error("OMP RPC frame exceeds the semantic byte limit"));
1994
+ return;
1995
+ }
1996
+ if (
1997
+ frame.index >= frame.count ||
1998
+ frame.byteLength > this.reassembledFrameLimit ||
1999
+ frame.data.length % 4 !== 0 ||
2000
+ !/^[A-Za-z0-9+/]*={0,2}$/u.test(frame.data)
2001
+ ) {
2002
+ this.rejectChunk();
2003
+ return;
2004
+ }
2005
+ const decoded = Buffer.from(frame.data, "base64");
2006
+ if (decoded.byteLength > MAX_CHUNK_BYTES) {
2007
+ this.rejectChunk();
2008
+ return;
2009
+ }
2010
+ if (!this.chunk) {
2011
+ if (frame.index !== 0) {
2012
+ this.rejectChunk();
2013
+ return;
2014
+ }
2015
+ this.chunk = {
2016
+ id: frame.chunkId,
2017
+ count: frame.count,
2018
+ byteLength: frame.byteLength,
2019
+ parts: [],
2020
+ receivedBytes: 0,
2021
+ timer: setTimeout(() => this.rejectChunk(), CHUNK_STALE_MS),
2022
+ };
2023
+ }
2024
+ const chunk = this.chunk;
2025
+ if (
2026
+ chunk.id !== frame.chunkId ||
2027
+ chunk.count !== frame.count ||
2028
+ chunk.byteLength !== frame.byteLength ||
2029
+ chunk.parts.length !== frame.index ||
2030
+ chunk.receivedBytes + decoded.byteLength > chunk.byteLength ||
2031
+ chunk.receivedBytes + decoded.byteLength > this.reassembledFrameLimit
2032
+ ) {
2033
+ this.rejectChunk();
2034
+ return;
2035
+ }
2036
+ chunk.parts.push(decoded);
2037
+ chunk.receivedBytes += decoded.byteLength;
2038
+ if (chunk.parts.length !== chunk.count) return;
2039
+ if (chunk.receivedBytes !== chunk.byteLength) {
2040
+ this.rejectChunk();
2041
+ return;
2042
+ }
2043
+ const reassembled = Buffer.concat(chunk.parts, chunk.receivedBytes);
2044
+ this.clearChunk();
2045
+ let decodedFrame: unknown;
2046
+ try {
2047
+ decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
2048
+ } catch {
2049
+ this.recordProtocolViolation();
2050
+ return;
2051
+ }
2052
+ if (this.receiveKnownResponse(decodedFrame)) return;
2053
+ if (this.receiveDegradedAgentEnd(decodedFrame, true)) return;
2054
+ if (
2055
+ boundedJsonBytes(
2056
+ decodedFrame,
2057
+ MAX_SEMANTIC_FRAME_BYTES,
2058
+ 1_024,
2059
+ MAX_IMAGE_DATA_LENGTH,
2060
+ 4_096,
2061
+ ) === Number.POSITIVE_INFINITY
2062
+ ) {
2063
+ this.recordProtocolViolation();
2064
+ return;
2065
+ }
2066
+ const frameObject = JsonObjectSchema.safeParse(decodedFrame);
2067
+ if (!frameObject.success) {
2068
+ this.recordProtocolViolation();
2069
+ return;
2070
+ }
2071
+ this.receiveFrame(frameObject.data);
2072
+ }
2073
+
2074
+ private receiveKnownResponse(value: unknown): boolean {
2075
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2076
+ const frame = value as Record<string, unknown>;
2077
+ if (frame.type !== "response" || typeof frame.id !== "string" || !this.pending.has(frame.id)) {
2078
+ return false;
2079
+ }
2080
+ this.receiveResponse(frame);
2081
+ return true;
2082
+ }
2083
+
2084
+ private receiveResponse(frame: Record<string, unknown>): void {
2085
+ const rawId = typeof frame.id === "string" ? frame.id : undefined;
2086
+ const knownPending = rawId ? this.pending.get(rawId) : undefined;
2087
+ const response = OmpResponseFrameSchema.safeParse(frame);
2088
+ if (!response.success) {
2089
+ if (rawId && knownPending) {
2090
+ this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
2091
+ } else {
2092
+ this.recordProtocolViolation();
2093
+ }
2094
+ return;
2095
+ }
2096
+ const pending = this.pending.get(response.data.id);
2097
+ if (!pending) {
2098
+ if (!response.data.success && this.acceptedPromptIds.delete(response.data.id)) {
2099
+ this.emit({
2100
+ type: "prompt_error",
2101
+ id: response.data.id,
2102
+ error: "OMP prompt scheduling failed",
2103
+ });
2104
+ }
2105
+ return;
2106
+ }
2107
+ const isBranchHistory = pending.command === "get_branch_messages";
2108
+ const isHistory =
2109
+ pending.command === "get_messages" || pending.command === "get_subagent_messages";
2110
+ const responseItemLimit = isBranchHistory ? 1_024 : isHistory ? 100_000 : MAX_ARRAY_ITEMS;
2111
+ const responseByteLimit =
2112
+ isBranchHistory || isHistory
2113
+ ? Math.min(MAX_REASSEMBLED_FRAME_BYTES, this.reassembledFrameLimit)
2114
+ : 2 * 1024 * 1024;
2115
+ const responseNodeLimit = isBranchHistory ? 4_096 : isHistory ? 400_000 : 2_048;
2116
+ if (
2117
+ boundedJsonBytes(
2118
+ frame,
2119
+ responseByteLimit,
2120
+ responseItemLimit,
2121
+ MAX_IMAGE_DATA_LENGTH,
2122
+ responseNodeLimit,
2123
+ ) === Number.POSITIVE_INFINITY
2124
+ ) {
2125
+ this.takePending(response.data.id)?.reject(
2126
+ new Error("OMP RPC response exceeded command limits"),
2127
+ );
2128
+ return;
2129
+ }
2130
+ const settled = this.takePending(response.data.id);
2131
+ if (!settled) return;
2132
+ if (response.data.success) {
2133
+ try {
2134
+ settled.beforeResolve?.(response.data.data);
2135
+ if (settled.command === "prompt") {
2136
+ if (this.acceptedPromptIds.size >= MAX_PENDING_REQUESTS) {
2137
+ const oldest = this.acceptedPromptIds.values().next().value;
2138
+ if (oldest !== undefined) this.acceptedPromptIds.delete(oldest);
2139
+ }
2140
+ this.acceptedPromptIds.add(response.data.id);
2141
+ }
2142
+ settled.resolve(response.data.data);
2143
+ } catch {
2144
+ settled.reject(new Error("OMP RPC response is invalid"));
2145
+ }
2146
+ } else {
2147
+ settled.reject(new Error("OMP RPC request failed"));
2148
+ }
2149
+ }
2150
+
2151
+ private takePending(id: string): PendingRequest | undefined {
2152
+ const pending = this.pending.get(id);
2153
+ if (!pending) return undefined;
2154
+ clearTimeout(pending.timer);
2155
+ this.pending.delete(id);
2156
+ return pending;
2157
+ }
2158
+
2159
+ private releaseQueuedWrite(id: string): void {
2160
+ const bytes = this.queuedWrites.get(id);
2161
+ if (bytes === undefined) return;
2162
+ this.queuedWrites.delete(id);
2163
+ this.pendingWriteBytes -= bytes;
2164
+ }
2165
+
2166
+ private receiveDegradedAgentEnd(value: unknown, onlyUnsafePayload: boolean): boolean {
2167
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2168
+ const frame = value as Record<string, unknown>;
2169
+ if (frame.type !== "agent_end") return false;
2170
+ const envelope = OmpAgentEndEnvelopeSchema.safeParse(frame);
2171
+ if (!envelope.success) {
2172
+ this.fail(new Error("OMP emitted invalid terminal metadata"));
2173
+ return true;
2174
+ }
2175
+ const messagesAreSafe =
2176
+ frame.messages === undefined ||
2177
+ (Array.isArray(frame.messages) &&
2178
+ frame.messages.length <= MAX_ARRAY_ITEMS &&
2179
+ boundedJsonBytes(
2180
+ frame.messages,
2181
+ MAX_SEMANTIC_FRAME_BYTES,
2182
+ MAX_ARRAY_ITEMS,
2183
+ MAX_TEXT_LENGTH,
2184
+ 4_096,
2185
+ ) !== Number.POSITIVE_INFINITY);
2186
+ const payloadIsSafe =
2187
+ messagesAreSafe &&
2188
+ boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) !==
2189
+ Number.POSITIVE_INFINITY;
2190
+ if (onlyUnsafePayload && payloadIsSafe) return false;
2191
+ if (envelope.data.isTerminal === false) {
2192
+ this.fail(new Error("OMP emitted an invalid nonterminal agent_end payload"));
2193
+ return true;
2194
+ }
2195
+ const observedCount = Array.isArray(frame.messages)
2196
+ ? frame.messages.length
2197
+ : Object.hasOwn(frame, "messages")
2198
+ ? 1
2199
+ : undefined;
2200
+ const messageCount = Math.max(envelope.data.messageCount ?? 0, observedCount ?? 0, 1);
2201
+ this.emit({
2202
+ ...envelope.data,
2203
+ messageCount,
2204
+ });
2205
+ this.streamedBlocks.clear();
2206
+ this.commandTextLength = 0;
2207
+ return true;
2208
+ }
2209
+
2210
+ private receiveFrame(frame: Record<string, unknown>): void {
2211
+ const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
2212
+ if (!type) {
2213
+ this.recordProtocolViolation();
2214
+ return;
2215
+ }
2216
+ if (this.receiveDegradedAgentEnd(frame, true)) return;
2217
+ if (
2218
+ boundedJsonBytes(frame, MAX_SEMANTIC_FRAME_BYTES, 1_024, MAX_IMAGE_DATA_LENGTH, 4_096) ===
2219
+ Number.POSITIVE_INFINITY
2220
+ ) {
2221
+ this.recordProtocolViolation();
2222
+ return;
2223
+ }
2224
+ if (type === "rpc_chunk") {
2225
+ const chunk = OmpChunkFrameSchema.safeParse(frame);
2226
+ if (!chunk.success) this.rejectChunk();
2227
+ else this.receiveChunk(chunk.data);
2228
+ return;
2229
+ }
2230
+ if (this.chunk) {
2231
+ this.clearChunk();
2232
+ this.recordProtocolViolation();
2233
+ }
2234
+ if (type === "rpc_frame_error") {
2235
+ this.recordProtocolViolation();
2236
+ return;
2237
+ }
2238
+ if (type === "ready") {
2239
+ if (this.readyReceived) {
2240
+ this.recordProtocolViolation();
2241
+ return;
2242
+ }
2243
+ const ready = OmpReadyFrameSchema.safeParse(frame);
2244
+ if (!ready.success) {
2245
+ this.recordProtocolViolation();
2246
+ } else {
2247
+ this.readyReceived = true;
2248
+ this.resolveReady(ready.data);
2249
+ }
2250
+ return;
2251
+ }
2252
+ if (type === "response") {
2253
+ this.receiveResponse(frame);
2254
+ return;
2255
+ }
2256
+ const event = OmpRuntimeEventSchema.safeParse(frame);
2257
+ if (!event.success) {
2258
+ this.rejectMatchingToolApproval(frame);
2259
+ if (type === "agent_end" && this.receiveDegradedAgentEnd(frame, false)) return;
2260
+ this.recordProtocolViolation();
2261
+ return;
2262
+ }
2263
+ if (!this.acceptEventState(event.data)) {
2264
+ this.recordProtocolViolation();
2265
+ return;
2266
+ }
2267
+ this.emit(event.data);
2268
+ if (event.data.type === "prompt_result" && event.data.id) {
2269
+ this.acceptedPromptIds.delete(event.data.id);
2270
+ }
2271
+ if (
2272
+ event.data.type === "message_end" ||
2273
+ event.data.type === "turn_end" ||
2274
+ event.data.type === "agent_end"
2275
+ ) {
2276
+ this.streamedBlocks.clear();
2277
+ }
2278
+ if (event.data.type === "turn_end") {
2279
+ this.commandTextLength = 0;
2280
+ this.activeToolCallIds.clear();
2281
+ }
2282
+ }
2283
+
2284
+ private rejectMatchingToolApproval(frame: Record<string, unknown>): void {
2285
+ if (frame.type !== "tool_approval_request") return;
2286
+ const { id, toolCallId } = frame;
2287
+ if (!isBoundedToolApprovalId(id) || !isBoundedToolApprovalId(toolCallId)) return;
2288
+ void this.sendFrame({
2289
+ type: "tool_approval_response",
2290
+ id,
2291
+ toolCallId,
2292
+ cancelled: true,
2293
+ }).catch(() => this.fail(new Error("OMP rejected tool approval could not be canceled")));
2294
+ }
2295
+
2296
+ private acceptEventState(event: z.infer<typeof OmpRuntimeEventSchema>): boolean {
2297
+ if (event.type === "turn_start") {
2298
+ this.streamedBlocks.clear();
2299
+ this.commandTextLength = 0;
2300
+ this.activeToolCallIds.clear();
2301
+ return true;
2302
+ }
2303
+ if (event.type === "command_output") {
2304
+ const nextLength = this.commandTextLength + utf8Bytes(event.text ?? "");
2305
+ if (nextLength > MAX_STREAM_TEXT_LENGTH) return false;
2306
+ this.commandTextLength = nextLength;
2307
+ return true;
2308
+ }
2309
+ if (event.type === "tool_execution_start") {
2310
+ if (
2311
+ !this.activeToolCallIds.has(event.toolCallId) &&
2312
+ this.activeToolCallIds.size >= MAX_ACTIVE_TOOLS
2313
+ ) {
2314
+ return false;
2315
+ }
2316
+ this.activeToolCallIds.add(event.toolCallId);
2317
+ return true;
2318
+ }
2319
+ if (event.type === "tool_execution_update") {
2320
+ return this.activeToolCallIds.has(event.toolCallId);
2321
+ }
2322
+ if (event.type === "tool_execution_end") {
2323
+ if (!this.activeToolCallIds.has(event.toolCallId)) return false;
2324
+ this.activeToolCallIds.delete(event.toolCallId);
2325
+ return true;
2326
+ }
2327
+ if (
2328
+ event.type !== "message_start" &&
2329
+ event.type !== "message_update" &&
2330
+ event.type !== "message_end"
2331
+ ) {
2332
+ return true;
2333
+ }
2334
+ if (event.message.role !== "assistant") return true;
2335
+ const nextBlocks =
2336
+ event.type === "message_start" ? new Map<number, string>() : new Map(this.streamedBlocks);
2337
+ const content = event.message.content;
2338
+ if (typeof content === "string") {
2339
+ nextBlocks.set(0, content);
2340
+ } else if (Array.isArray(content)) {
2341
+ for (const [index, part] of content.entries()) {
2342
+ const text =
2343
+ part.type === "text" ? part.text : part.type === "thinking" ? part.thinking : undefined;
2344
+ if (text !== undefined) nextBlocks.set(index, text);
2345
+ }
2346
+ }
2347
+ const update = event.type === "message_update" ? event.assistantMessageEvent : undefined;
2348
+ if (
2349
+ update?.contentIndex !== undefined &&
2350
+ update.delta !== undefined &&
2351
+ (content === undefined ||
2352
+ (Array.isArray(content) && content[update.contentIndex] === undefined))
2353
+ ) {
2354
+ const current = nextBlocks.get(update.contentIndex) ?? "";
2355
+ nextBlocks.set(update.contentIndex, `${current}${update.delta}`);
2356
+ }
2357
+ let totalLength = 0;
2358
+ for (const text of nextBlocks.values()) {
2359
+ totalLength += utf8Bytes(text);
2360
+ if (totalLength > MAX_STREAM_TEXT_LENGTH) return false;
2361
+ }
2362
+ this.streamedBlocks.clear();
2363
+ for (const [index, text] of nextBlocks) this.streamedBlocks.set(index, text);
2364
+ return true;
2365
+ }
2366
+
2367
+ private rejectChunk(): void {
2368
+ this.clearChunk();
2369
+ this.recordProtocolViolation();
2370
+ }
2371
+
2372
+ private recordProtocolViolation(): void {
2373
+ // Malformed bounded frames are isolated so the following frame starts from clean state.
2374
+ }
2375
+
2376
+ private fail(error: Error): void {
2377
+ if (this.fatalError || this.closed) return;
2378
+ this.fatalError = error;
2379
+ this.rejectReady(error);
2380
+ this.failPending(error);
2381
+ this.emit({ type: "process_exit", error: error.message });
2382
+ void this.close().catch(() => undefined);
2383
+ }
2384
+
2385
+ private clearChunk(): void {
2386
+ const chunk = this.chunk;
2387
+ if (chunk) clearTimeout(chunk.timer);
2388
+ this.chunk = null;
2389
+ }
2390
+
2391
+ private failPending(error: Error): void {
2392
+ for (const pending of this.pending.values()) {
2393
+ clearTimeout(pending.timer);
2394
+ pending.reject(error);
2395
+ }
2396
+ this.pending.clear();
2397
+ for (const pending of this.pendingOneWayWrites.values()) {
2398
+ clearTimeout(pending.timer);
2399
+ pending.reject(error);
2400
+ }
2401
+ this.acceptedPromptIds.clear();
2402
+ this.pendingOneWayWrites.clear();
2403
+ this.queuedWrites.clear();
2404
+ this.pendingWriteBytes = 0;
2405
+ }
2406
+
2407
+ private emit(event: OmpRpcEvent): void {
2408
+ for (const listener of this.listeners) listener(event);
2409
+ }
2410
+ }
2411
+
2412
+ function validateReadyMetadata(frame: ReadyFrame): void {
2413
+ const metadata = [
2414
+ frame.protocolVersion,
2415
+ frame.supportedProtocolVersions,
2416
+ frame.maxFrameBytes,
2417
+ frame.maxReassembledFrameBytes,
2418
+ ];
2419
+ if (metadata.every((value) => value === undefined)) {
2420
+ throw new OmpPublicError("OMP provider requires OMP RPC protocol v2");
2421
+ }
2422
+ if (metadata.some((value) => value === undefined)) {
2423
+ throw new Error("OMP ready frame contains incomplete protocol metadata");
2424
+ }
2425
+ if (
2426
+ frame.protocolVersion !== 1 ||
2427
+ !frame.supportedProtocolVersions?.includes(1) ||
2428
+ !frame.maxFrameBytes ||
2429
+ frame.maxFrameBytes > MAX_PHYSICAL_FRAME_BYTES ||
2430
+ !frame.maxReassembledFrameBytes ||
2431
+ frame.maxReassembledFrameBytes > MAX_REASSEMBLED_FRAME_BYTES ||
2432
+ frame.maxReassembledFrameBytes < frame.maxFrameBytes
2433
+ ) {
2434
+ throw new Error("OMP ready frame advertises unsupported protocol limits");
2435
+ }
2436
+ if (frame.maxFrameBytes < MIN_HOST_TOOL_RESULT_FRAME_BYTES) {
2437
+ throw new Error("OMP ready frame cannot carry terminal host tool results");
2438
+ }
2439
+ if (!frame.supportedProtocolVersions.includes(2)) {
2440
+ throw new OmpPublicError("OMP provider requires OMP RPC protocol v2");
2441
+ }
2442
+ }
2443
+
2444
+ class OmpRpcSession implements OmpRuntimeSession {
2445
+ get maxHostToolFrameBytes(): number {
2446
+ return this.process.outboundFrameLimit;
2447
+ }
2448
+ get maxInputFrameBytes(): number {
2449
+ return this.process.outboundFrameLimit;
2450
+ }
2451
+
2452
+ constructor(
2453
+ private readonly process: OmpRpcProcess,
2454
+ private readonly removeAbortListener: () => void,
2455
+ readonly canReplayHistory: boolean,
2456
+ readonly inheritedRedactionValues: readonly string[],
2457
+ readonly supportsTypedToolApprovals: boolean,
2458
+ ) {}
2459
+
2460
+ onEvent(listener: (event: OmpRpcEvent) => void): () => void {
2461
+ return this.process.onEvent(listener);
2462
+ }
2463
+
2464
+ async getState(): Promise<OmpSessionState> {
2465
+ return OmpSessionStateSchema.parse(await this.process.request({ type: "get_state" }));
2466
+ }
2467
+
2468
+ async getSessionStats(): Promise<OmpSessionStats> {
2469
+ return OmpSessionStatsSchema.parse(await this.process.request({ type: "get_session_stats" }));
2470
+ }
2471
+
2472
+ async compact(customInstructions?: string): Promise<OmpCompactionResult> {
2473
+ const instructions =
2474
+ customInstructions === undefined
2475
+ ? undefined
2476
+ : validateBoundedText(customInstructions, "compaction instructions", MAX_TEXT_LENGTH);
2477
+ return OmpCompactionResultSchema.parse(
2478
+ await this.process.request(
2479
+ {
2480
+ type: "compact",
2481
+ ...(instructions ? { customInstructions: instructions } : {}),
2482
+ },
2483
+ null,
2484
+ ),
2485
+ );
2486
+ }
2487
+ async setAutoCompaction(enabled: boolean): Promise<void> {
2488
+ await this.process.request({ type: "set_auto_compaction", enabled });
2489
+ }
2490
+
2491
+ async getAvailableModels(): Promise<OmpModel[]> {
2492
+ const result = OmpModelsResultSchema.parse(
2493
+ await this.process.request({ type: "get_available_models" }),
2494
+ );
2495
+ if (result.models.length === 0) throw new Error("OMP reported no available models");
2496
+ return result.models;
2497
+ }
2498
+
2499
+ async setModel(provider: string, modelId: string): Promise<OmpModel> {
2500
+ const safeProvider = validateBoundedText(provider, "model provider", MAX_NAME_LENGTH);
2501
+ const safeModelId = validateBoundedText(modelId, "model identifier", MAX_NAME_LENGTH);
2502
+ return OmpModelSchema.parse(
2503
+ await this.process.request({
2504
+ type: "set_model",
2505
+ provider: safeProvider,
2506
+ modelId: safeModelId,
2507
+ }),
2508
+ );
2509
+ }
2510
+
2511
+ async setThinkingLevel(level: string): Promise<void> {
2512
+ const parsed = OmpThinkingLevelSchema.parse(level);
2513
+ await this.process.request({ type: "set_thinking_level", level: parsed });
2514
+ }
2515
+
2516
+ async getAvailableCommands(): Promise<OmpAvailableCommand[]> {
2517
+ const result = OmpAvailableCommandsResultSchema.parse(
2518
+ await this.process.request({ type: "get_available_commands" }),
2519
+ );
2520
+ return result.commands;
2521
+ }
2522
+ async setSubagentSubscription(level: "events"): Promise<void> {
2523
+ await this.process.request({ type: "set_subagent_subscription", level });
2524
+ }
2525
+
2526
+ async getSubagents(): Promise<OmpSubagentSnapshot[]> {
2527
+ return OmpSubagentsResultSchema.parse(await this.process.request({ type: "get_subagents" }))
2528
+ .subagents;
2529
+ }
2530
+
2531
+ async getSubagentMessages(selector: {
2532
+ subagentId?: string;
2533
+ sessionFile?: string;
2534
+ }): Promise<OmpSubagentMessagesResult> {
2535
+ const subagentId = selector.subagentId
2536
+ ? validateBoundedText(selector.subagentId, "subagent identifier", MAX_ID_LENGTH)
2537
+ : undefined;
2538
+ const sessionFile = selector.sessionFile
2539
+ ? validateBoundedText(selector.sessionFile, "subagent transcript", MAX_PATH_LENGTH)
2540
+ : undefined;
2541
+ if ((subagentId ? 1 : 0) + (sessionFile ? 1 : 0) !== 1) {
2542
+ throw new OmpPublicError("OMP subagent history requires one transcript selector");
2543
+ }
2544
+ return OmpSubagentMessagesResultSchema.parse(
2545
+ await this.process.request({
2546
+ type: "get_subagent_messages",
2547
+ ...(subagentId ? { subagentId } : { sessionFile }),
2548
+ }),
2549
+ );
2550
+ }
2551
+
2552
+ async getBranchMessages(): Promise<Array<{ entryId: string; text: string }>> {
2553
+ const result = OmpBranchMessagesResultSchema.parse(
2554
+ await this.process.request({ type: "get_branch_messages" }),
2555
+ );
2556
+ return result.messages;
2557
+ }
2558
+ async branch(entryId: string): Promise<{ text: string; cancelled: boolean }> {
2559
+ const safeEntryId = validateBoundedText(entryId, "branch entry identifier", MAX_ID_LENGTH);
2560
+ const result = OmpBranchResultSchema.safeParse(
2561
+ await this.process.request({ type: "branch", entryId: safeEntryId }),
2562
+ );
2563
+ if (!result.success) throw new Error("OMP RPC response is invalid");
2564
+ return result.data;
2565
+ }
2566
+
2567
+ async getMessages(): Promise<OmpMessage[]> {
2568
+ if (!this.canReplayHistory) {
2569
+ throw new Error("OMP history replay requires negotiated RPC protocol v2");
2570
+ }
2571
+ const result = OmpMessagesResultSchema.parse(
2572
+ await this.process.request({ type: "get_messages" }),
2573
+ );
2574
+ return result.messages;
2575
+ }
2576
+
2577
+ async setHostTools(tools: readonly OmpHostToolDefinition[]): Promise<string[]> {
2578
+ const safeTools = z.array(OmpHostToolDefinitionSchema).max(MAX_HOST_TOOLS).parse(tools);
2579
+ if (safeTools.length === 0) return [];
2580
+ const result = z
2581
+ .object({ toolNames: z.array(NAME).max(MAX_HOST_TOOLS).optional() })
2582
+ .parse(await this.process.request({ type: "set_host_tools", tools: safeTools }));
2583
+ return result.toolNames ?? [];
2584
+ }
2585
+
2586
+ sendHostToolResult(result: OmpHostToolResult): void {
2587
+ this.process.send(result);
2588
+ }
2589
+
2590
+ sendHostToolUpdate(update: OmpHostToolUpdate): void {
2591
+ this.process.send(update);
2592
+ }
2593
+
2594
+ async prompt(
2595
+ message: string,
2596
+ images: readonly OmpImage[] = [],
2597
+ onAccepted?: () => void,
2598
+ ): Promise<{ requestId: string; agentInvoked?: boolean }> {
2599
+ const safeMessage = validateBoundedText(message, "prompt", MAX_TEXT_LENGTH);
2600
+ let acknowledgement: z.infer<typeof OmpPromptAckSchema> | undefined;
2601
+ const request = this.process.startRequest(
2602
+ { type: "prompt", message: safeMessage, ...(images.length > 0 ? { images } : {}) },
2603
+ undefined,
2604
+ (value) => {
2605
+ acknowledgement = OmpPromptAckSchema.parse(value) ?? {};
2606
+ onAccepted?.();
2607
+ },
2608
+ );
2609
+ await request.promise;
2610
+ return { requestId: request.id, ...acknowledgement };
2611
+ }
2612
+
2613
+ async steer(message: string, images: readonly OmpImage[] = []): Promise<void> {
2614
+ const safeMessage = validateBoundedText(message, "steer", MAX_TEXT_LENGTH);
2615
+ await this.process.sendFrame({
2616
+ type: "steer",
2617
+ message: safeMessage,
2618
+ ...(images.length > 0 ? { images } : {}),
2619
+ });
2620
+ }
2621
+ async followUp(message: string, images: readonly OmpImage[] = []): Promise<void> {
2622
+ const safeMessage = validateBoundedText(message, "follow-up", MAX_TEXT_LENGTH);
2623
+ await this.process.sendFrame({
2624
+ type: "follow_up",
2625
+ message: safeMessage,
2626
+ ...(images.length > 0 ? { images } : {}),
2627
+ });
2628
+ }
2629
+
2630
+ async handoff(customInstructions?: string): Promise<void> {
2631
+ const instructions =
2632
+ customInstructions === undefined
2633
+ ? undefined
2634
+ : validateBoundedText(customInstructions, "handoff instructions", MAX_TEXT_LENGTH);
2635
+ await this.process.request({
2636
+ type: "handoff",
2637
+ ...(instructions ? { customInstructions: instructions } : {}),
2638
+ });
2639
+ }
2640
+
2641
+ respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void> {
2642
+ return this.process.sendFrame(response);
2643
+ }
2644
+ respondToToolApproval(response: OmpToolApprovalResponse): Promise<void> {
2645
+ return this.process.sendFrame(OmpToolApprovalResponseSchema.parse(response));
2646
+ }
2647
+
2648
+ async abort(): Promise<void> {
2649
+ await this.process.request({ type: "abort", clearQueue: true, reason: "Interrupted in Paseo" });
2650
+ }
2651
+
2652
+ async close(): Promise<void> {
2653
+ this.removeAbortListener();
2654
+ await this.process.close();
2655
+ }
2656
+ }
2657
+
2658
+ export class OmpRpcRuntime implements OmpRuntime {
2659
+ readonly supportsPersistence = true;
2660
+ constructor(private readonly options: OmpRpcRuntimeOptions = {}) {}
2661
+ listSessions(options: OmpSessionListOptions): Promise<OmpSessionDescriptor[]> {
2662
+ return Promise.resolve(
2663
+ this.options.listSessions?.(options) ??
2664
+ listOmpSessionDescriptors(options, this.options.environment ?? process.env),
2665
+ );
2666
+ }
2667
+ async readPersistedSubagentTranscript(options: {
2668
+ parentSessionFile: string;
2669
+ childTranscriptId: string;
2670
+ cwd: string;
2671
+ signal?: AbortSignal;
2672
+ }): Promise<OmpPersistedSubagentMessages> {
2673
+ const transcript = await readOmpPersistedSubagentTranscript(
2674
+ options.parentSessionFile,
2675
+ options.childTranscriptId,
2676
+ options.cwd,
2677
+ options.signal,
2678
+ );
2679
+ return {
2680
+ ...transcript,
2681
+ messages: z.array(OmpMessageSchema).max(100_000).parse(transcript.messages),
2682
+ };
2683
+ }
2684
+
2685
+ async startSession(options: OmpStartOptions): Promise<OmpRuntimeSession> {
2686
+ options.signal?.throwIfAborted();
2687
+ const effectiveOptions = {
2688
+ ...options,
2689
+ environment: options.environment ?? this.options.environment,
2690
+ };
2691
+ const process = new OmpRpcProcess(
2692
+ effectiveOptions,
2693
+ this.options.spawnProcess,
2694
+ this.options.terminateProcessTree,
2695
+ options.requestTimeoutMs ?? this.options.requestTimeoutMs,
2696
+ );
2697
+ const abort = () => void process.close().catch(() => undefined);
2698
+ options.signal?.addEventListener("abort", abort, { once: true });
2699
+ const removeAbortListener = () => options.signal?.removeEventListener("abort", abort);
2700
+ try {
2701
+ const readyTimeoutMs = options.readyTimeoutMs ?? READY_TIMEOUT_MS;
2702
+ const ready = await waitWithTimeout(
2703
+ process.ready,
2704
+ readyTimeoutMs,
2705
+ `OMP RPC did not become ready within ${readyTimeoutMs}ms`,
2706
+ );
2707
+ validateReadyMetadata(ready);
2708
+ process.applyReadyLimits(ready);
2709
+ options.signal?.throwIfAborted();
2710
+ const advertiseTypedToolApprovals = ready.features?.typedToolApprovals === 1;
2711
+ const negotiation = ProtocolNegotiationResultSchema.parse(
2712
+ await process.request({
2713
+ type: "negotiate_protocol",
2714
+ protocolVersion: 2,
2715
+ ...(advertiseTypedToolApprovals
2716
+ ? { clientCapabilities: { typedToolApprovals: 1 as const } }
2717
+ : {}),
2718
+ }),
2719
+ );
2720
+ options.signal?.throwIfAborted();
2721
+ return new OmpRpcSession(
2722
+ process,
2723
+ removeAbortListener,
2724
+ true,
2725
+ process.inheritedRedactionValues,
2726
+ advertiseTypedToolApprovals && negotiation.clientCapabilities?.typedToolApprovals === 1,
2727
+ );
2728
+ } catch (error) {
2729
+ removeAbortListener();
2730
+ const cleanup = process.close();
2731
+ try {
2732
+ await cleanup;
2733
+ } catch {
2734
+ throw new OmpCleanupFailure("OMP runtime startup cleanup failed", cleanup);
2735
+ }
2736
+ throw error;
2737
+ }
2738
+ }
2739
+ }