@omercnet/paseo-omp 0.3.0-next.104.1 → 0.3.0-next.105.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/provider/omp-rpc.ts +97 -19
package/package.json
CHANGED
|
@@ -85,6 +85,32 @@ const MAX_COST_USD = 1_000_000_000;
|
|
|
85
85
|
const MAX_RPC_ERROR_BYTES = 4_096;
|
|
86
86
|
const MAX_RPC_ERROR_CODE_BYTES = 256;
|
|
87
87
|
const PROMPT_SCHEDULING_FAILURE = "OMP prompt scheduling failed";
|
|
88
|
+
const PROTOCOL_VIOLATION_COALESCE_MS = 10_000;
|
|
89
|
+
|
|
90
|
+
export type OmpProtocolViolationCategory =
|
|
91
|
+
| "duplicate-ready"
|
|
92
|
+
| "frame-limit"
|
|
93
|
+
| "incomplete-frame"
|
|
94
|
+
| "interleaved-chunk"
|
|
95
|
+
| "invalid-chunk"
|
|
96
|
+
| "invalid-envelope"
|
|
97
|
+
| "invalid-event"
|
|
98
|
+
| "invalid-event-state"
|
|
99
|
+
| "invalid-json"
|
|
100
|
+
| "invalid-ready"
|
|
101
|
+
| "invalid-response"
|
|
102
|
+
| "remote-frame-error";
|
|
103
|
+
|
|
104
|
+
export interface OmpProtocolViolationDiagnostic {
|
|
105
|
+
category: OmpProtocolViolationCategory;
|
|
106
|
+
occurrenceCount: number;
|
|
107
|
+
frameType?: "ready" | "response" | "rpc_chunk" | "rpc_frame_error";
|
|
108
|
+
maxByteSize?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type PendingProtocolViolation = Omit<OmpProtocolViolationDiagnostic, "occurrenceCount"> & {
|
|
112
|
+
occurrenceCount: number;
|
|
113
|
+
};
|
|
88
114
|
const MAX_CONTEXT_PERCENT = 1_000_000;
|
|
89
115
|
function boundedJsonString(maxBytes: number, minBytes = 0) {
|
|
90
116
|
return z.string().refine((value) => {
|
|
@@ -1439,6 +1465,7 @@ export interface OmpRpcRuntimeOptions {
|
|
|
1439
1465
|
terminateProcessTree?: (pid: number) => Promise<boolean | "uncertain">;
|
|
1440
1466
|
environment?: NodeJS.ProcessEnv;
|
|
1441
1467
|
requestTimeoutMs?: number;
|
|
1468
|
+
reportProtocolViolation?: (diagnostic: OmpProtocolViolationDiagnostic) => void | Promise<void>;
|
|
1442
1469
|
listSessions?: (
|
|
1443
1470
|
options: OmpSessionListOptions,
|
|
1444
1471
|
) => OmpSessionDescriptor[] | Promise<OmpSessionDescriptor[]>;
|
|
@@ -1970,12 +1997,20 @@ class OmpRpcProcess {
|
|
|
1970
1997
|
private spawnFailedWithoutProcess = false;
|
|
1971
1998
|
private readyReceived = false;
|
|
1972
1999
|
private outputSettled = false;
|
|
2000
|
+
private readonly pendingProtocolViolations = new Map<
|
|
2001
|
+
OmpProtocolViolationCategory,
|
|
2002
|
+
PendingProtocolViolation
|
|
2003
|
+
>();
|
|
2004
|
+
private protocolViolationTimer: TimerHandle | null = null;
|
|
1973
2005
|
|
|
1974
2006
|
constructor(
|
|
1975
2007
|
options: OmpStartOptions,
|
|
1976
2008
|
spawnProcess?: OmpRpcRuntimeOptions["spawnProcess"],
|
|
1977
2009
|
terminateProcessTree?: OmpRpcRuntimeOptions["terminateProcessTree"],
|
|
1978
2010
|
private readonly requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
2011
|
+
private readonly reportProtocolViolation: (
|
|
2012
|
+
diagnostic: OmpProtocolViolationDiagnostic,
|
|
2013
|
+
) => void | Promise<void> = (diagnostic) => console.error("OMP protocol violation", diagnostic),
|
|
1979
2014
|
) {
|
|
1980
2015
|
const ready = Promise.withResolvers<ReadyFrame>();
|
|
1981
2016
|
this.rejectReady = ready.reject;
|
|
@@ -2064,7 +2099,11 @@ class OmpRpcProcess {
|
|
|
2064
2099
|
private settleOutput(): void {
|
|
2065
2100
|
if (this.outputSettled) return;
|
|
2066
2101
|
this.outputSettled = true;
|
|
2067
|
-
if (this.lineBytes > 0 || this.discardingLine)
|
|
2102
|
+
if (this.lineBytes > 0 || this.discardingLine) {
|
|
2103
|
+
this.recordProtocolViolation("incomplete-frame", {
|
|
2104
|
+
maxByteSize: this.discardingLine ? this.discardedLineBytes : this.lineBytes,
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2068
2107
|
this.lineParts = [];
|
|
2069
2108
|
this.lineBytes = 0;
|
|
2070
2109
|
this.discardingLine = false;
|
|
@@ -2247,6 +2286,7 @@ class OmpRpcProcess {
|
|
|
2247
2286
|
|
|
2248
2287
|
private async closeProcess(): Promise<void> {
|
|
2249
2288
|
this.closed = true;
|
|
2289
|
+
this.flushProtocolViolations();
|
|
2250
2290
|
this.clearChunk();
|
|
2251
2291
|
this.failPending(new Error("OMP RPC process was closed"));
|
|
2252
2292
|
if (!this.exited) {
|
|
@@ -2343,7 +2383,7 @@ class OmpRpcProcess {
|
|
|
2343
2383
|
this.lineParts = [];
|
|
2344
2384
|
this.lineBytes = 0;
|
|
2345
2385
|
this.discardingLine = true;
|
|
2346
|
-
this.recordProtocolViolation();
|
|
2386
|
+
this.recordProtocolViolation("frame-limit", { maxByteSize: nextBytes });
|
|
2347
2387
|
return;
|
|
2348
2388
|
}
|
|
2349
2389
|
this.lineParts.push(part);
|
|
@@ -2364,7 +2404,7 @@ class OmpRpcProcess {
|
|
|
2364
2404
|
try {
|
|
2365
2405
|
decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
|
|
2366
2406
|
} catch {
|
|
2367
|
-
this.recordProtocolViolation();
|
|
2407
|
+
this.recordProtocolViolation("invalid-json", { maxByteSize: payload.byteLength });
|
|
2368
2408
|
return;
|
|
2369
2409
|
}
|
|
2370
2410
|
this.receiveDecodedFrame(decoded, payload.byteLength);
|
|
@@ -2434,7 +2474,7 @@ class OmpRpcProcess {
|
|
|
2434
2474
|
try {
|
|
2435
2475
|
decodedFrame = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(reassembled));
|
|
2436
2476
|
} catch {
|
|
2437
|
-
this.recordProtocolViolation();
|
|
2477
|
+
this.recordProtocolViolation("invalid-json", { maxByteSize: reassembled.byteLength });
|
|
2438
2478
|
return;
|
|
2439
2479
|
}
|
|
2440
2480
|
this.receiveDecodedFrame(decodedFrame, reassembled.byteLength);
|
|
@@ -2460,10 +2500,10 @@ class OmpRpcProcess {
|
|
|
2460
2500
|
const sanitized = sanitizeLiveDisplayFrame(value);
|
|
2461
2501
|
const frame = JsonObjectSchema.safeParse(sanitized);
|
|
2462
2502
|
if (!frame.success) {
|
|
2463
|
-
this.recordProtocolViolation();
|
|
2503
|
+
this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
|
|
2464
2504
|
return;
|
|
2465
2505
|
}
|
|
2466
|
-
this.receiveFrame(frame.data);
|
|
2506
|
+
this.receiveFrame(frame.data, rawByteLength);
|
|
2467
2507
|
}
|
|
2468
2508
|
|
|
2469
2509
|
private receiveKnownResponse(value: unknown): boolean {
|
|
@@ -2488,7 +2528,7 @@ class OmpRpcProcess {
|
|
|
2488
2528
|
if (rawId && knownPending) {
|
|
2489
2529
|
this.takePending(rawId)?.reject(new Error("OMP RPC response is invalid"));
|
|
2490
2530
|
} else if (!rawId || !this.emitAcceptedPromptFailure(rawId, frame)) {
|
|
2491
|
-
this.recordProtocolViolation();
|
|
2531
|
+
this.recordProtocolViolation("invalid-response", { frameType: "response" });
|
|
2492
2532
|
}
|
|
2493
2533
|
return;
|
|
2494
2534
|
}
|
|
@@ -2643,10 +2683,10 @@ class OmpRpcProcess {
|
|
|
2643
2683
|
return true;
|
|
2644
2684
|
}
|
|
2645
2685
|
|
|
2646
|
-
private receiveFrame(frame: Record<string, unknown
|
|
2686
|
+
private receiveFrame(frame: Record<string, unknown>, rawByteLength: number): void {
|
|
2647
2687
|
const type = typeof frame.type === "string" && frame.type.length <= 64 ? frame.type : null;
|
|
2648
2688
|
if (!type) {
|
|
2649
|
-
this.recordProtocolViolation();
|
|
2689
|
+
this.recordProtocolViolation("invalid-envelope", { maxByteSize: rawByteLength });
|
|
2650
2690
|
return;
|
|
2651
2691
|
}
|
|
2652
2692
|
const safeFrame = mapRuntimeFrameDetails(frame, createOptionalMetadataSanitizer());
|
|
@@ -2660,7 +2700,7 @@ class OmpRpcProcess {
|
|
|
2660
2700
|
4_096,
|
|
2661
2701
|
) === Number.POSITIVE_INFINITY
|
|
2662
2702
|
) {
|
|
2663
|
-
this.recordProtocolViolation();
|
|
2703
|
+
this.recordProtocolViolation("frame-limit", { maxByteSize: rawByteLength });
|
|
2664
2704
|
return;
|
|
2665
2705
|
}
|
|
2666
2706
|
if (type === "rpc_chunk") {
|
|
@@ -2671,20 +2711,20 @@ class OmpRpcProcess {
|
|
|
2671
2711
|
}
|
|
2672
2712
|
if (this.chunk) {
|
|
2673
2713
|
this.clearChunk();
|
|
2674
|
-
this.recordProtocolViolation();
|
|
2714
|
+
this.recordProtocolViolation("interleaved-chunk");
|
|
2675
2715
|
}
|
|
2676
2716
|
if (type === "rpc_frame_error") {
|
|
2677
|
-
this.recordProtocolViolation();
|
|
2717
|
+
this.recordProtocolViolation("remote-frame-error", { frameType: "rpc_frame_error" });
|
|
2678
2718
|
return;
|
|
2679
2719
|
}
|
|
2680
2720
|
if (type === "ready") {
|
|
2681
2721
|
if (this.readyReceived) {
|
|
2682
|
-
this.recordProtocolViolation();
|
|
2722
|
+
this.recordProtocolViolation("duplicate-ready", { frameType: "ready" });
|
|
2683
2723
|
return;
|
|
2684
2724
|
}
|
|
2685
2725
|
const ready = OmpReadyFrameSchema.safeParse(safeFrame);
|
|
2686
2726
|
if (!ready.success) {
|
|
2687
|
-
this.recordProtocolViolation();
|
|
2727
|
+
this.recordProtocolViolation("invalid-ready", { frameType: "ready" });
|
|
2688
2728
|
} else {
|
|
2689
2729
|
this.readyReceived = true;
|
|
2690
2730
|
this.resolveReady(ready.data);
|
|
@@ -2699,11 +2739,11 @@ class OmpRpcProcess {
|
|
|
2699
2739
|
if (!event.success) {
|
|
2700
2740
|
this.rejectMatchingToolApproval(safeFrame);
|
|
2701
2741
|
if (type === "agent_end" && this.receiveDegradedAgentEnd(safeFrame, false)) return;
|
|
2702
|
-
this.recordProtocolViolation();
|
|
2742
|
+
this.recordProtocolViolation("invalid-event", { maxByteSize: rawByteLength });
|
|
2703
2743
|
return;
|
|
2704
2744
|
}
|
|
2705
2745
|
if (!this.acceptEventState(event.data)) {
|
|
2706
|
-
this.recordProtocolViolation();
|
|
2746
|
+
this.recordProtocolViolation("invalid-event-state", { maxByteSize: rawByteLength });
|
|
2707
2747
|
return;
|
|
2708
2748
|
}
|
|
2709
2749
|
this.emit(event.data);
|
|
@@ -2808,11 +2848,48 @@ class OmpRpcProcess {
|
|
|
2808
2848
|
|
|
2809
2849
|
private rejectChunk(): void {
|
|
2810
2850
|
this.clearChunk();
|
|
2811
|
-
this.recordProtocolViolation();
|
|
2851
|
+
this.recordProtocolViolation("invalid-chunk", { frameType: "rpc_chunk" });
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
private recordProtocolViolation(
|
|
2855
|
+
category: OmpProtocolViolationCategory,
|
|
2856
|
+
metadata: Omit<OmpProtocolViolationDiagnostic, "category" | "occurrenceCount"> = {},
|
|
2857
|
+
): void {
|
|
2858
|
+
if (!this.protocolViolationTimer) {
|
|
2859
|
+
this.emitProtocolViolation({ category, occurrenceCount: 1, ...metadata });
|
|
2860
|
+
this.protocolViolationTimer = setTimeout(
|
|
2861
|
+
() => this.flushProtocolViolations(),
|
|
2862
|
+
PROTOCOL_VIOLATION_COALESCE_MS,
|
|
2863
|
+
);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const pending = this.pendingProtocolViolations.get(category);
|
|
2867
|
+
if (!pending) {
|
|
2868
|
+
this.pendingProtocolViolations.set(category, { category, occurrenceCount: 1, ...metadata });
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
pending.occurrenceCount = Math.min(Number.MAX_SAFE_INTEGER, pending.occurrenceCount + 1);
|
|
2872
|
+
pending.maxByteSize =
|
|
2873
|
+
Math.max(pending.maxByteSize ?? 0, metadata.maxByteSize ?? 0) || undefined;
|
|
2874
|
+
if (pending.frameType !== metadata.frameType) pending.frameType = undefined;
|
|
2812
2875
|
}
|
|
2813
2876
|
|
|
2814
|
-
private
|
|
2815
|
-
|
|
2877
|
+
private flushProtocolViolations(): void {
|
|
2878
|
+
if (this.protocolViolationTimer) clearTimeout(this.protocolViolationTimer);
|
|
2879
|
+
this.protocolViolationTimer = null;
|
|
2880
|
+
for (const diagnostic of this.pendingProtocolViolations.values()) {
|
|
2881
|
+
this.emitProtocolViolation(diagnostic);
|
|
2882
|
+
}
|
|
2883
|
+
this.pendingProtocolViolations.clear();
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
private emitProtocolViolation(diagnostic: OmpProtocolViolationDiagnostic): void {
|
|
2887
|
+
try {
|
|
2888
|
+
const reporting = this.reportProtocolViolation(diagnostic);
|
|
2889
|
+
if (reporting) void reporting.catch(() => undefined);
|
|
2890
|
+
} catch {
|
|
2891
|
+
// Diagnostics must never alter transport flow or cleanup.
|
|
2892
|
+
}
|
|
2816
2893
|
}
|
|
2817
2894
|
|
|
2818
2895
|
private fail(error: Error): void {
|
|
@@ -3161,6 +3238,7 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
3161
3238
|
this.options.spawnProcess,
|
|
3162
3239
|
this.options.terminateProcessTree,
|
|
3163
3240
|
options.requestTimeoutMs ?? this.options.requestTimeoutMs,
|
|
3241
|
+
this.options.reportProtocolViolation,
|
|
3164
3242
|
);
|
|
3165
3243
|
const abort = () => void process.close().catch(() => undefined);
|
|
3166
3244
|
options.signal?.addEventListener("abort", abort, { once: true });
|