@omercnet/paseo-omp 0.4.0 → 1.0.0-next.148.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/client/external-url.ts +2 -11
- package/client/omp-config-styles.ts +284 -0
- package/client/omp-config-surface.tsx +16 -286
- package/client/omp-routing-editor.tsx +1 -3
- package/client/provider-launch-settings.tsx +118 -0
- package/index.client.tsx +21 -2
- package/index.server.ts +10 -0
- package/package.json +14 -9
- package/paseo-plugin.json +2 -1
- package/scripts/prepare-dependencies.mjs +44 -11
- package/server/omp-models.ts +2 -6
- package/server/package-version.ts +1 -1
- package/server/protocol-violation-diagnostics.ts +1 -1
- package/server/provider/catalog.ts +2 -1
- package/server/provider/config-normalization.ts +19 -3
- package/server/provider/connection.ts +30 -0
- package/server/provider/host-tools.ts +2 -2
- package/server/provider/mcp-transport.ts +9 -2
- package/server/provider/omp-rpc-environment.ts +368 -0
- package/server/provider/omp-rpc-process.ts +119 -0
- package/server/provider/omp-rpc-protocol.ts +1949 -0
- package/server/provider/omp-rpc-transport.ts +1373 -0
- package/server/provider/omp-rpc-values.ts +31 -0
- package/server/provider/omp-rpc.ts +267 -3311
- package/server/provider/profile-providers.ts +2 -1
- package/server/provider/prompt-payload.ts +253 -0
- package/server/provider/registration.ts +10 -2
- package/server/provider/session-compaction.ts +187 -0
- package/server/provider/session-descriptors.ts +52 -18
- package/server/provider/session-events.ts +42 -0
- package/server/provider/session-permissions.ts +987 -0
- package/server/provider/session-terminal.ts +307 -0
- package/server/provider/session-usage.ts +234 -0
- package/server/provider/session.ts +145 -1853
- package/server/provider/settings.ts +8 -5
- package/server/provider/subsessions.ts +30 -9
- package/server/provider/timeline-projector.ts +140 -29
- package/shared/external-url.ts +0 -9
- package/shared/provider-launch-settings.ts +18 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ProviderEvent,
|
|
4
|
+
ProviderInput,
|
|
5
|
+
ProviderPermissionResponse,
|
|
6
|
+
ProviderToolCallDetail,
|
|
7
|
+
} from "@getpaseo/plugin/server/provider";
|
|
8
|
+
import type { OmpRuntimeSession } from "./omp-rpc";
|
|
9
|
+
import type {
|
|
10
|
+
OmpExtensionUiResponse,
|
|
11
|
+
OmpRpcEvent,
|
|
12
|
+
OmpToolApprovalCancel,
|
|
13
|
+
OmpToolApprovalRequest,
|
|
14
|
+
} from "./omp-rpc-protocol";
|
|
15
|
+
import {
|
|
16
|
+
BoundedStringSet,
|
|
17
|
+
boundedJsonBytes,
|
|
18
|
+
type OmpPublicDataSerializer,
|
|
19
|
+
OmpPublicError,
|
|
20
|
+
utf8Bytes,
|
|
21
|
+
} from "./security";
|
|
22
|
+
import type { ActiveTurn } from "./session-terminal";
|
|
23
|
+
import type { OmpTimelineProjector, OmpTimelineScheduler } from "./timeline-projector";
|
|
24
|
+
|
|
25
|
+
type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
|
|
26
|
+
type OmpQuestionRequest = Extract<
|
|
27
|
+
Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
|
|
28
|
+
{ method: "select" | "confirm" | "input" | "editor" }
|
|
29
|
+
>;
|
|
30
|
+
type Emit = (event: ProviderEvent) => void;
|
|
31
|
+
|
|
32
|
+
const MAX_PENDING_PERMISSIONS = 32;
|
|
33
|
+
const MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
|
|
34
|
+
const MAX_RESOLVED_TOOL_APPROVAL_IDS = 1_024;
|
|
35
|
+
const OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
|
|
36
|
+
const MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
|
|
37
|
+
|
|
38
|
+
type PendingPermission = {
|
|
39
|
+
nativeId: string;
|
|
40
|
+
header: string;
|
|
41
|
+
fingerprint: string;
|
|
42
|
+
optionValues: ReadonlyMap<string, string>;
|
|
43
|
+
actionBehaviors: ReadonlyMap<string, "allow" | "deny">;
|
|
44
|
+
retainedBytes: number;
|
|
45
|
+
displayValues: ReadonlyMap<string, string>;
|
|
46
|
+
generation: number;
|
|
47
|
+
runtime: OmpRuntimeSession;
|
|
48
|
+
expiresAt?: number;
|
|
49
|
+
timer?: unknown;
|
|
50
|
+
turnId?: string;
|
|
51
|
+
request: OmpQuestionRequest;
|
|
52
|
+
freeformSentinel?: string;
|
|
53
|
+
};
|
|
54
|
+
type PendingToolPermission = {
|
|
55
|
+
nativeId: string;
|
|
56
|
+
toolCallId: string;
|
|
57
|
+
fingerprint: string;
|
|
58
|
+
retainedBytes: number;
|
|
59
|
+
generation: number;
|
|
60
|
+
runtime: OmpRuntimeSession;
|
|
61
|
+
expiresAt?: number;
|
|
62
|
+
timer?: unknown;
|
|
63
|
+
turnId?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type PendingFreeformSelection = {
|
|
67
|
+
value: string;
|
|
68
|
+
nativeSelectId: string;
|
|
69
|
+
generation: number;
|
|
70
|
+
runtime: OmpRuntimeSession;
|
|
71
|
+
turnId?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function permissionFingerprint(request: OmpQuestionRequest): string {
|
|
75
|
+
return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface PermissionContext {
|
|
79
|
+
runtime: OmpRuntimeSession;
|
|
80
|
+
generation: number;
|
|
81
|
+
activeTurn: ActiveTurn | null;
|
|
82
|
+
closed: boolean;
|
|
83
|
+
runtimeDead: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface PermissionHost {
|
|
87
|
+
sessionId: string;
|
|
88
|
+
emit: Emit;
|
|
89
|
+
scheduler: OmpTimelineScheduler;
|
|
90
|
+
dataFilter: OmpPublicDataSerializer;
|
|
91
|
+
projector: OmpTimelineProjector;
|
|
92
|
+
readContext(): PermissionContext;
|
|
93
|
+
handleRuntimeFailure(message?: string): void;
|
|
94
|
+
invalidateRuntime(message: string): void;
|
|
95
|
+
markAgentEvidence(turn: ActiveTurn): void;
|
|
96
|
+
reevaluateDeferredTerminal(): void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class OmpSessionPermissions {
|
|
100
|
+
private permissionSequence = 0;
|
|
101
|
+
private readonly permissionNamespace = randomUUID();
|
|
102
|
+
private readonly pendingPermissions = new Map<string, PendingPermission>();
|
|
103
|
+
private readonly inFlightPermissions = new Map<string, PendingPermission>();
|
|
104
|
+
private pendingFreeformSelection: PendingFreeformSelection | null = null;
|
|
105
|
+
private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
|
|
106
|
+
private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
|
|
107
|
+
private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_RESOLVED_TOOL_APPROVAL_IDS);
|
|
108
|
+
|
|
109
|
+
constructor(private readonly host: PermissionHost) {}
|
|
110
|
+
|
|
111
|
+
private get id(): string {
|
|
112
|
+
return this.host.sessionId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private get runtime(): OmpRuntimeSession {
|
|
116
|
+
return this.host.readContext().runtime;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private get generation(): number {
|
|
120
|
+
return this.host.readContext().generation;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private get activeTurn(): ActiveTurn | null {
|
|
124
|
+
return this.host.readContext().activeTurn;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private get closed(): boolean {
|
|
128
|
+
return this.host.readContext().closed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private get runtimeDead(): boolean {
|
|
132
|
+
return this.host.readContext().runtimeDead;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private get scheduler(): OmpTimelineScheduler {
|
|
136
|
+
return this.host.scheduler;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private get dataFilter(): OmpPublicDataSerializer {
|
|
140
|
+
return this.host.dataFilter;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private get projector(): OmpTimelineProjector {
|
|
144
|
+
return this.host.projector;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private emit(event: ProviderEvent): void {
|
|
148
|
+
this.host.emit(event);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private handleRuntimeFailure(message?: string): void {
|
|
152
|
+
this.host.handleRuntimeFailure(message);
|
|
153
|
+
}
|
|
154
|
+
private invalidateRuntime(message: string): void {
|
|
155
|
+
this.host.invalidateRuntime(message);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private markAgentEvidence(turn: ActiveTurn): void {
|
|
159
|
+
this.host.markAgentEvidence(turn);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private reevaluateDeferredPermissionTerminal(): void {
|
|
163
|
+
this.host.reevaluateDeferredTerminal();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async respond(input: SessionPermissionInput): Promise<void> {
|
|
167
|
+
const typed = this.pendingToolPermissions.get(input.permissionId);
|
|
168
|
+
if (typed) {
|
|
169
|
+
await this.respondToToolPermission(input, typed);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const pending = this.pendingPermissions.get(input.permissionId);
|
|
173
|
+
if (!pending) throw new OmpPublicError("Unknown OMP permission request");
|
|
174
|
+
if (!this.permissionOwnerIsCurrent(pending)) {
|
|
175
|
+
this.pendingPermissions.delete(input.permissionId);
|
|
176
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
177
|
+
this.emit({
|
|
178
|
+
type: "session.permission_resolved",
|
|
179
|
+
sessionId: this.id,
|
|
180
|
+
permissionId: input.permissionId,
|
|
181
|
+
});
|
|
182
|
+
throw new OmpPublicError("OMP permission request is no longer active");
|
|
183
|
+
}
|
|
184
|
+
const { generation, runtime } = pending;
|
|
185
|
+
this.pendingPermissions.delete(input.permissionId);
|
|
186
|
+
this.inFlightPermissions.set(input.permissionId, pending);
|
|
187
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
188
|
+
try {
|
|
189
|
+
const freeformValue = this.freeformSelection(pending, input.response);
|
|
190
|
+
if (freeformValue !== undefined) {
|
|
191
|
+
this.pendingFreeformSelection = {
|
|
192
|
+
value: freeformValue,
|
|
193
|
+
nativeSelectId: pending.nativeId,
|
|
194
|
+
generation,
|
|
195
|
+
runtime,
|
|
196
|
+
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const response = this.extensionUiResponse(pending, input.response);
|
|
200
|
+
await runtime.respondToExtensionUi(response);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (this.pendingFreeformSelection?.nativeSelectId === pending.nativeId) {
|
|
203
|
+
this.pendingFreeformSelection = null;
|
|
204
|
+
}
|
|
205
|
+
if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
|
|
206
|
+
this.inFlightPermissions.delete(input.permissionId);
|
|
207
|
+
if (
|
|
208
|
+
this.permissionOwnerIsCurrent(pending) &&
|
|
209
|
+
generation === this.generation &&
|
|
210
|
+
runtime === this.runtime
|
|
211
|
+
) {
|
|
212
|
+
this.pendingPermissions.set(input.permissionId, pending);
|
|
213
|
+
this.armPermissionTimeout(input.permissionId, pending);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
|
|
219
|
+
this.inFlightPermissions.delete(input.permissionId);
|
|
220
|
+
this.emit({
|
|
221
|
+
type: "session.permission_resolved",
|
|
222
|
+
sessionId: this.id,
|
|
223
|
+
permissionId: input.permissionId,
|
|
224
|
+
});
|
|
225
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
226
|
+
}
|
|
227
|
+
private async respondToToolPermission(
|
|
228
|
+
input: SessionPermissionInput,
|
|
229
|
+
pending: PendingToolPermission,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
if (!this.permissionOwnerIsCurrent(pending)) {
|
|
232
|
+
this.pendingToolPermissions.delete(input.permissionId);
|
|
233
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
234
|
+
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
235
|
+
this.emit({
|
|
236
|
+
type: "session.permission_resolved",
|
|
237
|
+
sessionId: this.id,
|
|
238
|
+
permissionId: input.permissionId,
|
|
239
|
+
});
|
|
240
|
+
throw new OmpPublicError("OMP permission request is no longer active");
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
input.response.selectedActionId !== undefined &&
|
|
244
|
+
input.response.selectedActionId !== input.response.behavior
|
|
245
|
+
) {
|
|
246
|
+
throw new OmpPublicError("OMP permission action is invalid");
|
|
247
|
+
}
|
|
248
|
+
this.pendingToolPermissions.delete(input.permissionId);
|
|
249
|
+
this.inFlightToolPermissions.set(input.permissionId, pending);
|
|
250
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
251
|
+
try {
|
|
252
|
+
await pending.runtime.respondToToolApproval({
|
|
253
|
+
type: "tool_approval_response",
|
|
254
|
+
id: pending.nativeId,
|
|
255
|
+
toolCallId: pending.toolCallId,
|
|
256
|
+
approved: input.response.behavior === "allow",
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
|
|
260
|
+
this.inFlightToolPermissions.delete(input.permissionId);
|
|
261
|
+
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
262
|
+
this.emit({
|
|
263
|
+
type: "session.permission_resolved",
|
|
264
|
+
sessionId: this.id,
|
|
265
|
+
permissionId: input.permissionId,
|
|
266
|
+
});
|
|
267
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
|
|
271
|
+
this.inFlightToolPermissions.delete(input.permissionId);
|
|
272
|
+
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
273
|
+
this.emit({
|
|
274
|
+
type: "session.permission_resolved",
|
|
275
|
+
sessionId: this.id,
|
|
276
|
+
permissionId: input.permissionId,
|
|
277
|
+
});
|
|
278
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
submitPendingFreeformSelection(
|
|
282
|
+
request: Extract<OmpQuestionRequest, { method: "input" }>,
|
|
283
|
+
): boolean {
|
|
284
|
+
const pending = this.pendingFreeformSelection;
|
|
285
|
+
if (!pending) return false;
|
|
286
|
+
this.pendingFreeformSelection = null;
|
|
287
|
+
if (
|
|
288
|
+
pending.generation !== this.generation ||
|
|
289
|
+
pending.runtime !== this.runtime ||
|
|
290
|
+
(pending.turnId !== undefined && this.activeTurn?.turnId !== pending.turnId)
|
|
291
|
+
) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
void pending.runtime
|
|
295
|
+
.respondToExtensionUi({ type: "extension_ui_response", id: request.id, value: pending.value })
|
|
296
|
+
.catch(() => this.handleRuntimeFailure("OMP freeform response failed"));
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
private toolPermissionDetail(
|
|
300
|
+
request: OmpToolApprovalRequest,
|
|
301
|
+
): ProviderToolCallDetail | undefined {
|
|
302
|
+
switch (request.identity.kind) {
|
|
303
|
+
case "shell":
|
|
304
|
+
return {
|
|
305
|
+
type: "shell",
|
|
306
|
+
command: this.dataFilter.text(request.identity.command, 24 * 1024),
|
|
307
|
+
};
|
|
308
|
+
case "edit":
|
|
309
|
+
return {
|
|
310
|
+
type: "edit",
|
|
311
|
+
filePath: this.dataFilter.text(request.identity.paths[0] ?? "", 4_096),
|
|
312
|
+
newString: this.dataFilter.text(request.identity.content, 20 * 1024),
|
|
313
|
+
};
|
|
314
|
+
case "write":
|
|
315
|
+
return {
|
|
316
|
+
type: "write",
|
|
317
|
+
filePath: this.dataFilter.text(request.identity.path, 4_096),
|
|
318
|
+
content: this.dataFilter.text(request.identity.content, 20 * 1024),
|
|
319
|
+
};
|
|
320
|
+
case "other":
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
publishToolPermission(request: OmpToolApprovalRequest): void {
|
|
326
|
+
const fingerprint = createHash("sha256").update(JSON.stringify(request)).digest("base64url");
|
|
327
|
+
if (this.resolvedToolApprovalIds.has(request.id)) {
|
|
328
|
+
this.handleRuntimeFailure("OMP reused a resolved tool approval identifier");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
for (const pending of [
|
|
332
|
+
...this.pendingToolPermissions.values(),
|
|
333
|
+
...this.inFlightToolPermissions.values(),
|
|
334
|
+
]) {
|
|
335
|
+
if (pending.nativeId !== request.id) continue;
|
|
336
|
+
if (pending.fingerprint !== fingerprint || pending.toolCallId !== request.toolCallId) {
|
|
337
|
+
this.handleRuntimeFailure("OMP changed a pending tool approval request");
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const pendingCount =
|
|
342
|
+
this.pendingPermissions.size +
|
|
343
|
+
this.inFlightPermissions.size +
|
|
344
|
+
this.pendingToolPermissions.size +
|
|
345
|
+
this.inFlightToolPermissions.size;
|
|
346
|
+
if (pendingCount >= MAX_PENDING_PERMISSIONS) {
|
|
347
|
+
this.resolvedToolApprovalIds.add(request.id);
|
|
348
|
+
void this.runtime
|
|
349
|
+
.respondToToolApproval({
|
|
350
|
+
type: "tool_approval_response",
|
|
351
|
+
id: request.id,
|
|
352
|
+
toolCallId: request.toolCallId,
|
|
353
|
+
cancelled: true,
|
|
354
|
+
})
|
|
355
|
+
.catch(() => this.handleRuntimeFailure());
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.permissionSequence += 1;
|
|
360
|
+
const permissionId = `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
|
|
361
|
+
const detail = this.toolPermissionDetail(request);
|
|
362
|
+
const filteredInput = this.dataFilter.json(request.input, 8 * 1024, 32 * 1024);
|
|
363
|
+
const input =
|
|
364
|
+
filteredInput && typeof filteredInput === "object" && !Array.isArray(filteredInput)
|
|
365
|
+
? filteredInput
|
|
366
|
+
: {};
|
|
367
|
+
const descriptionParts = [
|
|
368
|
+
request.detail.reason,
|
|
369
|
+
...request.detail.lines,
|
|
370
|
+
...(request.detail.providerSafetyChecks ?? []),
|
|
371
|
+
].filter((part): part is string => Boolean(part));
|
|
372
|
+
const publicRequest = {
|
|
373
|
+
id: permissionId,
|
|
374
|
+
name: `omp.${this.dataFilter.text(request.toolName, 256)}`,
|
|
375
|
+
kind: "tool" as const,
|
|
376
|
+
title: `Allow ${this.dataFilter.text(request.toolName, 256)}?`,
|
|
377
|
+
...(descriptionParts.length > 0
|
|
378
|
+
? { description: this.dataFilter.text(descriptionParts.join("\n"), 16 * 1024) }
|
|
379
|
+
: {}),
|
|
380
|
+
input: {
|
|
381
|
+
...input,
|
|
382
|
+
identity: this.dataFilter.json(request.identity, 20 * 1024, 32 * 1024),
|
|
383
|
+
tier: request.tier,
|
|
384
|
+
toolCallId: request.toolCallId,
|
|
385
|
+
},
|
|
386
|
+
...(detail ? { detail } : {}),
|
|
387
|
+
actions: [
|
|
388
|
+
{ id: "allow", label: "Allow", behavior: "allow" as const, variant: "primary" as const },
|
|
389
|
+
{ id: "deny", label: "Deny", behavior: "deny" as const, variant: "danger" as const },
|
|
390
|
+
],
|
|
391
|
+
metadata: {
|
|
392
|
+
tier: request.tier,
|
|
393
|
+
redacted: request.detail.redacted,
|
|
394
|
+
truncated: request.detail.truncated,
|
|
395
|
+
redactedFields: request.detail.redactedFields,
|
|
396
|
+
truncatedFields: request.detail.truncatedFields,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
const pending: PendingToolPermission = {
|
|
400
|
+
nativeId: request.id,
|
|
401
|
+
toolCallId: request.toolCallId,
|
|
402
|
+
fingerprint,
|
|
403
|
+
retainedBytes: boundedJsonBytes(
|
|
404
|
+
publicRequest,
|
|
405
|
+
MAX_PENDING_PERMISSION_BYTES,
|
|
406
|
+
512,
|
|
407
|
+
MAX_PENDING_PERMISSION_BYTES,
|
|
408
|
+
4_096,
|
|
409
|
+
),
|
|
410
|
+
generation: this.generation,
|
|
411
|
+
runtime: this.runtime,
|
|
412
|
+
...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
|
|
413
|
+
...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
|
|
414
|
+
};
|
|
415
|
+
let retainedBytes = pending.retainedBytes;
|
|
416
|
+
for (const item of this.pendingPermissions.values()) retainedBytes += item.retainedBytes;
|
|
417
|
+
for (const item of this.inFlightPermissions.values()) retainedBytes += item.retainedBytes;
|
|
418
|
+
for (const item of this.pendingToolPermissions.values()) retainedBytes += item.retainedBytes;
|
|
419
|
+
for (const item of this.inFlightToolPermissions.values()) retainedBytes += item.retainedBytes;
|
|
420
|
+
if (
|
|
421
|
+
pending.retainedBytes === Number.POSITIVE_INFINITY ||
|
|
422
|
+
retainedBytes > MAX_PENDING_PERMISSION_BYTES
|
|
423
|
+
) {
|
|
424
|
+
void this.runtime
|
|
425
|
+
.respondToToolApproval({
|
|
426
|
+
type: "tool_approval_response",
|
|
427
|
+
id: request.id,
|
|
428
|
+
toolCallId: request.toolCallId,
|
|
429
|
+
cancelled: true,
|
|
430
|
+
})
|
|
431
|
+
.catch(() => this.handleRuntimeFailure());
|
|
432
|
+
this.resolvedToolApprovalIds.add(request.id);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
this.pendingToolPermissions.set(permissionId, pending);
|
|
436
|
+
this.armToolPermissionTimeout(permissionId, pending);
|
|
437
|
+
this.projector.markAskPermissionRendered();
|
|
438
|
+
if (this.activeTurn) {
|
|
439
|
+
this.activeTurn.awaitingPermissionEvidence = true;
|
|
440
|
+
this.markAgentEvidence(this.activeTurn);
|
|
441
|
+
}
|
|
442
|
+
this.emit({ type: "session.permission", sessionId: this.id, request: publicRequest });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
cancelToolPermission(request: OmpToolApprovalCancel): void {
|
|
446
|
+
for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
|
|
447
|
+
for (const [permissionId, pending] of permissions) {
|
|
448
|
+
if (pending.nativeId !== request.targetId) continue;
|
|
449
|
+
if (pending.toolCallId !== request.toolCallId) {
|
|
450
|
+
this.handleRuntimeFailure("OMP tool approval cancellation did not match its tool call");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
permissions.delete(permissionId);
|
|
454
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
455
|
+
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
456
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
457
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!this.resolvedToolApprovalIds.has(request.targetId)) {
|
|
462
|
+
this.handleRuntimeFailure("OMP canceled an unknown tool approval request");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
publishPermission(request: OmpQuestionRequest): void {
|
|
467
|
+
if (request.method === "select" && !request.options?.length) {
|
|
468
|
+
this.handleRuntimeFailure();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const fingerprint = permissionFingerprint(request);
|
|
472
|
+
let existingId: string | undefined;
|
|
473
|
+
let existingPending: PendingPermission | undefined;
|
|
474
|
+
for (const [permissionId, pending] of this.pendingPermissions) {
|
|
475
|
+
if (pending.nativeId !== request.id) continue;
|
|
476
|
+
if (pending.fingerprint === fingerprint) {
|
|
477
|
+
existingId = permissionId;
|
|
478
|
+
existingPending = pending;
|
|
479
|
+
} else {
|
|
480
|
+
this.pendingPermissions.delete(permissionId);
|
|
481
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
482
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
if (!existingId) {
|
|
487
|
+
for (const pending of this.inFlightPermissions.values()) {
|
|
488
|
+
if (pending.nativeId !== request.id) continue;
|
|
489
|
+
if (pending.fingerprint !== fingerprint) this.handleRuntimeFailure();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (
|
|
494
|
+
!existingId &&
|
|
495
|
+
this.pendingPermissions.size +
|
|
496
|
+
this.inFlightPermissions.size +
|
|
497
|
+
this.pendingToolPermissions.size +
|
|
498
|
+
this.inFlightToolPermissions.size >=
|
|
499
|
+
MAX_PENDING_PERMISSIONS
|
|
500
|
+
) {
|
|
501
|
+
this.rejectPermissionRequest(request, "Too many OMP questions are already pending");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (existingPending?.timer !== undefined) this.scheduler.clear(existingPending.timer);
|
|
505
|
+
if (!existingId) this.permissionSequence += 1;
|
|
506
|
+
const id =
|
|
507
|
+
existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
|
|
508
|
+
const header = this.dataFilter.text(request.title ?? "OMP question", 4_096);
|
|
509
|
+
const freeformSentinel =
|
|
510
|
+
request.method === "select" && request.options.includes(OMP_ASK_USER_FREEFORM_SENTINEL)
|
|
511
|
+
? OMP_ASK_USER_FREEFORM_SENTINEL
|
|
512
|
+
: undefined;
|
|
513
|
+
const optionValues = new Map<string, string>();
|
|
514
|
+
const displayValues = new Map<string, string>();
|
|
515
|
+
const usedOptionLabels = new Set<string>();
|
|
516
|
+
const optionDetails = request.method === "select" ? request.optionDetails : undefined;
|
|
517
|
+
const selectableOptions =
|
|
518
|
+
request.method === "select"
|
|
519
|
+
? request.options.flatMap((nativeValue, index) =>
|
|
520
|
+
nativeValue === freeformSentinel ? [] : [{ nativeValue, index }],
|
|
521
|
+
)
|
|
522
|
+
: [];
|
|
523
|
+
const options =
|
|
524
|
+
request.method === "select"
|
|
525
|
+
? selectableOptions.map(({ nativeValue, index }) => {
|
|
526
|
+
const baseLabel = this.dataFilter.text(nativeValue, 4_096);
|
|
527
|
+
let label = baseLabel;
|
|
528
|
+
let suffix = 2;
|
|
529
|
+
while (usedOptionLabels.has(label)) {
|
|
530
|
+
label = `${baseLabel} (${suffix})`;
|
|
531
|
+
suffix += 1;
|
|
532
|
+
}
|
|
533
|
+
usedOptionLabels.add(label);
|
|
534
|
+
const value = `${id}:option:${index}`;
|
|
535
|
+
optionValues.set(value, nativeValue);
|
|
536
|
+
displayValues.set(label, nativeValue);
|
|
537
|
+
return {
|
|
538
|
+
label,
|
|
539
|
+
value,
|
|
540
|
+
...(optionDetails?.[index]?.description
|
|
541
|
+
? {
|
|
542
|
+
description: this.dataFilter.text(
|
|
543
|
+
optionDetails[index]?.description ?? "",
|
|
544
|
+
16_384,
|
|
545
|
+
),
|
|
546
|
+
}
|
|
547
|
+
: {}),
|
|
548
|
+
};
|
|
549
|
+
})
|
|
550
|
+
: undefined;
|
|
551
|
+
const questionOptions =
|
|
552
|
+
request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
|
|
553
|
+
const actions =
|
|
554
|
+
request.method === "select"
|
|
555
|
+
? [
|
|
556
|
+
...(options ?? []).map((option) => ({
|
|
557
|
+
id: option.value,
|
|
558
|
+
label: option.label,
|
|
559
|
+
behavior: "allow" as const,
|
|
560
|
+
variant: "secondary" as const,
|
|
561
|
+
})),
|
|
562
|
+
{
|
|
563
|
+
id: "cancel",
|
|
564
|
+
label: "Cancel",
|
|
565
|
+
behavior: "deny" as const,
|
|
566
|
+
variant: "secondary" as const,
|
|
567
|
+
},
|
|
568
|
+
]
|
|
569
|
+
: [
|
|
570
|
+
{
|
|
571
|
+
id: "submit",
|
|
572
|
+
label: request.method === "confirm" ? "Confirm" : "Submit",
|
|
573
|
+
behavior: "allow" as const,
|
|
574
|
+
variant: "primary" as const,
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
id: "cancel",
|
|
578
|
+
label: "Cancel",
|
|
579
|
+
behavior: "deny" as const,
|
|
580
|
+
variant: "secondary" as const,
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
const pending: PendingPermission = {
|
|
584
|
+
nativeId: request.id,
|
|
585
|
+
header,
|
|
586
|
+
fingerprint,
|
|
587
|
+
optionValues,
|
|
588
|
+
actionBehaviors: new Map(actions.map((action) => [action.id, action.behavior])),
|
|
589
|
+
displayValues,
|
|
590
|
+
generation: this.generation,
|
|
591
|
+
runtime: this.runtime,
|
|
592
|
+
request,
|
|
593
|
+
retainedBytes: boundedJsonBytes(
|
|
594
|
+
{ request, header, options: questionOptions, actions },
|
|
595
|
+
MAX_PENDING_PERMISSION_BYTES,
|
|
596
|
+
512,
|
|
597
|
+
MAX_PENDING_PERMISSION_BYTES,
|
|
598
|
+
4_096,
|
|
599
|
+
),
|
|
600
|
+
...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
|
|
601
|
+
...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
|
|
602
|
+
...(freeformSentinel ? { freeformSentinel } : {}),
|
|
603
|
+
};
|
|
604
|
+
let retainedPermissionBytes = pending.retainedBytes;
|
|
605
|
+
for (const [permissionId, retained] of this.pendingPermissions) {
|
|
606
|
+
if (permissionId !== existingId) retainedPermissionBytes += retained.retainedBytes;
|
|
607
|
+
}
|
|
608
|
+
for (const retained of this.inFlightPermissions.values()) {
|
|
609
|
+
retainedPermissionBytes += retained.retainedBytes;
|
|
610
|
+
}
|
|
611
|
+
for (const retained of this.pendingToolPermissions.values()) {
|
|
612
|
+
retainedPermissionBytes += retained.retainedBytes;
|
|
613
|
+
}
|
|
614
|
+
for (const retained of this.inFlightToolPermissions.values()) {
|
|
615
|
+
retainedPermissionBytes += retained.retainedBytes;
|
|
616
|
+
}
|
|
617
|
+
if (
|
|
618
|
+
pending.retainedBytes === Number.POSITIVE_INFINITY ||
|
|
619
|
+
retainedPermissionBytes > MAX_PENDING_PERMISSION_BYTES
|
|
620
|
+
) {
|
|
621
|
+
if (existingId) {
|
|
622
|
+
this.pendingPermissions.delete(existingId);
|
|
623
|
+
this.emit({
|
|
624
|
+
type: "session.permission_resolved",
|
|
625
|
+
sessionId: this.id,
|
|
626
|
+
permissionId: existingId,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
this.rejectPermissionRequest(request, "OMP question data exceeded the pending input budget");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
this.pendingPermissions.set(id, pending);
|
|
633
|
+
this.armPermissionTimeout(id, pending);
|
|
634
|
+
this.projector.markAskPermissionRendered();
|
|
635
|
+
if (this.activeTurn) {
|
|
636
|
+
this.activeTurn.awaitingPermissionEvidence = true;
|
|
637
|
+
this.markAgentEvidence(this.activeTurn);
|
|
638
|
+
}
|
|
639
|
+
this.emit({
|
|
640
|
+
type: "session.permission",
|
|
641
|
+
sessionId: this.id,
|
|
642
|
+
request: {
|
|
643
|
+
id,
|
|
644
|
+
name: `omp.${request.method}`,
|
|
645
|
+
kind: "question",
|
|
646
|
+
title: header,
|
|
647
|
+
...(request.method === "confirm"
|
|
648
|
+
? { description: this.dataFilter.text(request.message, 64 * 1024) }
|
|
649
|
+
: {}),
|
|
650
|
+
input: {
|
|
651
|
+
questions: [
|
|
652
|
+
{
|
|
653
|
+
header,
|
|
654
|
+
question: this.dataFilter.text(
|
|
655
|
+
request.method === "confirm" ? request.message : request.title,
|
|
656
|
+
64 * 1024,
|
|
657
|
+
),
|
|
658
|
+
options: questionOptions,
|
|
659
|
+
multiSelect: false,
|
|
660
|
+
...(freeformSentinel ? { allowOther: true } : {}),
|
|
661
|
+
...(request.method === "input" && request.placeholder
|
|
662
|
+
? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
|
|
663
|
+
: {}),
|
|
664
|
+
...((request.method === "input" || request.method === "editor") && request.prefill
|
|
665
|
+
? { prefill: this.dataFilter.text(request.prefill) }
|
|
666
|
+
: {}),
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
actions,
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
private rejectPermissionRequest(request: OmpQuestionRequest, description: string): void {
|
|
676
|
+
this.emit({
|
|
677
|
+
type: "session.notice",
|
|
678
|
+
sessionId: this.id,
|
|
679
|
+
notice: {
|
|
680
|
+
id: `omp:permission-rejected:${this.permissionSequence + 1}`,
|
|
681
|
+
severity: "warning",
|
|
682
|
+
title: "OMP question canceled",
|
|
683
|
+
description,
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
void this.runtime
|
|
687
|
+
.respondToExtensionUi({ type: "extension_ui_response", id: request.id, cancelled: true })
|
|
688
|
+
.catch(() => this.handleRuntimeFailure());
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private freeformSelection(
|
|
692
|
+
pending: PendingPermission,
|
|
693
|
+
response: ProviderPermissionResponse,
|
|
694
|
+
): string | undefined {
|
|
695
|
+
if (
|
|
696
|
+
pending.request.method !== "select" ||
|
|
697
|
+
!pending.freeformSentinel ||
|
|
698
|
+
response.behavior !== "allow" ||
|
|
699
|
+
response.selectedActionId !== undefined
|
|
700
|
+
) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const answers = response.updatedInput?.answers;
|
|
704
|
+
const answer =
|
|
705
|
+
answers && typeof answers === "object" && !Array.isArray(answers)
|
|
706
|
+
? answers[pending.header]
|
|
707
|
+
: undefined;
|
|
708
|
+
const value = Array.isArray(answer) ? answer[0] : answer;
|
|
709
|
+
if (
|
|
710
|
+
typeof value !== "string" ||
|
|
711
|
+
pending.optionValues.has(value) ||
|
|
712
|
+
pending.displayValues.has(value)
|
|
713
|
+
) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (
|
|
717
|
+
value === pending.freeformSentinel ||
|
|
718
|
+
value.trim().length === 0 ||
|
|
719
|
+
value.includes("\0") ||
|
|
720
|
+
utf8Bytes(value) > MAX_FREEFORM_RESPONSE_BYTES
|
|
721
|
+
) {
|
|
722
|
+
throw new OmpPublicError("OMP freeform response is invalid");
|
|
723
|
+
}
|
|
724
|
+
return value;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private extensionUiResponse(
|
|
728
|
+
pending: PendingPermission,
|
|
729
|
+
response: ProviderPermissionResponse,
|
|
730
|
+
): OmpExtensionUiResponse {
|
|
731
|
+
if (response.selectedActionId !== undefined) {
|
|
732
|
+
const expectedBehavior = pending.actionBehaviors.get(response.selectedActionId);
|
|
733
|
+
if (expectedBehavior === undefined || expectedBehavior !== response.behavior) {
|
|
734
|
+
throw new OmpPublicError("OMP permission action is invalid");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const { nativeId, request, header } = pending;
|
|
738
|
+
if (request.method === "confirm") {
|
|
739
|
+
if (response.behavior === "deny") {
|
|
740
|
+
return { type: "extension_ui_response", id: nativeId, confirmed: false };
|
|
741
|
+
}
|
|
742
|
+
const answers = response.updatedInput?.answers;
|
|
743
|
+
const answer =
|
|
744
|
+
answers && typeof answers === "object" && !Array.isArray(answers)
|
|
745
|
+
? answers[header]
|
|
746
|
+
: undefined;
|
|
747
|
+
return {
|
|
748
|
+
type: "extension_ui_response",
|
|
749
|
+
id: nativeId,
|
|
750
|
+
confirmed: typeof answer === "string" ? /^yes$/iu.test(answer.trim()) : true,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
754
|
+
if (response.behavior === "deny") {
|
|
755
|
+
return { type: "extension_ui_response", id: nativeId, cancelled: true };
|
|
756
|
+
}
|
|
757
|
+
const selectedValue = response.selectedActionId
|
|
758
|
+
? pending.optionValues.get(response.selectedActionId)
|
|
759
|
+
: undefined;
|
|
760
|
+
if (selectedValue !== undefined) {
|
|
761
|
+
return { type: "extension_ui_response", id: nativeId, value: selectedValue };
|
|
762
|
+
}
|
|
763
|
+
const answers = response.updatedInput?.answers;
|
|
764
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
|
|
765
|
+
throw new OmpPublicError("OMP question response requires answers");
|
|
766
|
+
}
|
|
767
|
+
const answer = answers[header];
|
|
768
|
+
const publicValue = Array.isArray(answer) ? answer[0] : answer;
|
|
769
|
+
if (typeof publicValue !== "string") {
|
|
770
|
+
throw new OmpPublicError("OMP question response is invalid");
|
|
771
|
+
}
|
|
772
|
+
if (
|
|
773
|
+
request.method === "select" &&
|
|
774
|
+
pending.freeformSentinel &&
|
|
775
|
+
publicValue !== pending.freeformSentinel &&
|
|
776
|
+
!pending.displayValues.has(publicValue) &&
|
|
777
|
+
!pending.optionValues.has(publicValue)
|
|
778
|
+
) {
|
|
779
|
+
if (
|
|
780
|
+
publicValue.trim().length === 0 ||
|
|
781
|
+
publicValue.includes("\0") ||
|
|
782
|
+
utf8Bytes(publicValue) > MAX_FREEFORM_RESPONSE_BYTES
|
|
783
|
+
) {
|
|
784
|
+
throw new OmpPublicError("OMP freeform response is invalid");
|
|
785
|
+
}
|
|
786
|
+
this.pendingFreeformSelection = {
|
|
787
|
+
value: publicValue,
|
|
788
|
+
nativeSelectId: pending.nativeId,
|
|
789
|
+
generation: pending.generation,
|
|
790
|
+
runtime: pending.runtime,
|
|
791
|
+
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
792
|
+
};
|
|
793
|
+
return {
|
|
794
|
+
type: "extension_ui_response",
|
|
795
|
+
id: nativeId,
|
|
796
|
+
value: pending.freeformSentinel,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (request.method === "select") {
|
|
800
|
+
const mapped =
|
|
801
|
+
pending.optionValues.get(publicValue) ?? pending.displayValues.get(publicValue);
|
|
802
|
+
if (mapped === undefined) {
|
|
803
|
+
throw new OmpPublicError("OMP selection response is invalid");
|
|
804
|
+
}
|
|
805
|
+
return { type: "extension_ui_response", id: nativeId, value: mapped };
|
|
806
|
+
}
|
|
807
|
+
return { type: "extension_ui_response", id: nativeId, value: publicValue };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
resolvePermissionByNativeId(nativeId: string): void {
|
|
811
|
+
for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
|
|
812
|
+
for (const [permissionId, pending] of permissions) {
|
|
813
|
+
if (pending.nativeId !== nativeId) continue;
|
|
814
|
+
permissions.delete(permissionId);
|
|
815
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
816
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
817
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
resolveTurnPermissions(turnId: string): void {
|
|
824
|
+
if (this.pendingFreeformSelection?.turnId === turnId) this.pendingFreeformSelection = null;
|
|
825
|
+
this.resolvePermissions((pending) => pending.turnId === turnId, true);
|
|
826
|
+
this.resolveToolPermissions((pending) => pending.turnId === turnId, true);
|
|
827
|
+
}
|
|
828
|
+
resolveAllPermissions(cancelNative = false): void {
|
|
829
|
+
this.pendingFreeformSelection = null;
|
|
830
|
+
this.resolvePermissions(() => true, cancelNative);
|
|
831
|
+
this.resolveToolPermissions(() => true, cancelNative);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private resolvePermissions(
|
|
835
|
+
matches: (pending: PendingPermission) => boolean,
|
|
836
|
+
cancelNative: boolean,
|
|
837
|
+
): void {
|
|
838
|
+
const permissionIds = new Set<string>();
|
|
839
|
+
for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
|
|
840
|
+
for (const [permissionId, pending] of permissions) {
|
|
841
|
+
if (!matches(pending)) continue;
|
|
842
|
+
permissions.delete(permissionId);
|
|
843
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
844
|
+
permissionIds.add(permissionId);
|
|
845
|
+
if (cancelNative && permissions === this.pendingPermissions) {
|
|
846
|
+
void pending.runtime
|
|
847
|
+
.respondToExtensionUi({
|
|
848
|
+
type: "extension_ui_response",
|
|
849
|
+
id: pending.nativeId,
|
|
850
|
+
cancelled: true,
|
|
851
|
+
})
|
|
852
|
+
.catch(() => {
|
|
853
|
+
if (!this.closed && !this.runtimeDead) {
|
|
854
|
+
this.invalidateRuntime("OMP permission cancellation failed");
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const permissionId of permissionIds) {
|
|
861
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
862
|
+
}
|
|
863
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
864
|
+
}
|
|
865
|
+
private resolveToolPermissions(
|
|
866
|
+
matches: (pending: PendingToolPermission) => boolean,
|
|
867
|
+
cancelNative: boolean,
|
|
868
|
+
): void {
|
|
869
|
+
const permissionIds = new Set<string>();
|
|
870
|
+
for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
|
|
871
|
+
for (const [permissionId, pending] of permissions) {
|
|
872
|
+
if (!matches(pending)) continue;
|
|
873
|
+
permissions.delete(permissionId);
|
|
874
|
+
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
875
|
+
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
876
|
+
permissionIds.add(permissionId);
|
|
877
|
+
if (cancelNative && permissions === this.pendingToolPermissions) {
|
|
878
|
+
void pending.runtime
|
|
879
|
+
.respondToToolApproval({
|
|
880
|
+
type: "tool_approval_response",
|
|
881
|
+
id: pending.nativeId,
|
|
882
|
+
toolCallId: pending.toolCallId,
|
|
883
|
+
cancelled: true,
|
|
884
|
+
})
|
|
885
|
+
.catch(() => {
|
|
886
|
+
if (!this.closed && !this.runtimeDead) {
|
|
887
|
+
this.invalidateRuntime("OMP tool permission cancellation failed");
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
for (const permissionId of permissionIds) {
|
|
894
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
895
|
+
}
|
|
896
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private armToolPermissionTimeout(permissionId: string, pending: PendingToolPermission): void {
|
|
900
|
+
if (pending.expiresAt === undefined) return;
|
|
901
|
+
const remainingMs = Math.max(0, pending.expiresAt - Date.now());
|
|
902
|
+
pending.timer = this.scheduler.set(() => {
|
|
903
|
+
if (this.pendingToolPermissions.get(permissionId) !== pending) return;
|
|
904
|
+
this.pendingToolPermissions.delete(permissionId);
|
|
905
|
+
this.inFlightToolPermissions.set(permissionId, pending);
|
|
906
|
+
void pending.runtime
|
|
907
|
+
.respondToToolApproval({
|
|
908
|
+
type: "tool_approval_response",
|
|
909
|
+
id: pending.nativeId,
|
|
910
|
+
toolCallId: pending.toolCallId,
|
|
911
|
+
cancelled: true,
|
|
912
|
+
timedOut: true,
|
|
913
|
+
})
|
|
914
|
+
.then(
|
|
915
|
+
() => {
|
|
916
|
+
if (this.inFlightToolPermissions.get(permissionId) !== pending) return;
|
|
917
|
+
this.inFlightToolPermissions.delete(permissionId);
|
|
918
|
+
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
919
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
920
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
921
|
+
},
|
|
922
|
+
() => this.handleRuntimeFailure(),
|
|
923
|
+
);
|
|
924
|
+
}, remainingMs);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private armPermissionTimeout(permissionId: string, pending: PendingPermission): void {
|
|
928
|
+
if (pending.expiresAt === undefined) return;
|
|
929
|
+
const remainingMs = Math.max(0, pending.expiresAt - Date.now());
|
|
930
|
+
pending.timer = this.scheduler.set(() => {
|
|
931
|
+
if (this.pendingPermissions.get(permissionId) !== pending) return;
|
|
932
|
+
this.pendingPermissions.delete(permissionId);
|
|
933
|
+
if (!this.permissionOwnerIsCurrent(pending)) {
|
|
934
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
935
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
this.inFlightPermissions.set(permissionId, pending);
|
|
939
|
+
void pending.runtime
|
|
940
|
+
.respondToExtensionUi({
|
|
941
|
+
type: "extension_ui_response",
|
|
942
|
+
id: pending.nativeId,
|
|
943
|
+
cancelled: true,
|
|
944
|
+
timedOut: true,
|
|
945
|
+
})
|
|
946
|
+
.then(
|
|
947
|
+
() => {
|
|
948
|
+
if (this.inFlightPermissions.get(permissionId) !== pending) return;
|
|
949
|
+
this.inFlightPermissions.delete(permissionId);
|
|
950
|
+
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
951
|
+
this.reevaluateDeferredPermissionTerminal();
|
|
952
|
+
},
|
|
953
|
+
() => this.handleRuntimeFailure(),
|
|
954
|
+
);
|
|
955
|
+
}, remainingMs);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private permissionOwnerIsCurrent(pending: PendingPermission | PendingToolPermission): boolean {
|
|
959
|
+
if (
|
|
960
|
+
this.closed ||
|
|
961
|
+
this.runtimeDead ||
|
|
962
|
+
pending.generation !== this.generation ||
|
|
963
|
+
pending.runtime !== this.runtime
|
|
964
|
+
) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
if (pending.turnId === undefined) return true;
|
|
968
|
+
return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
hasForTurn(turnId: string): boolean {
|
|
972
|
+
const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
|
|
973
|
+
pending.turnId === turnId;
|
|
974
|
+
return (
|
|
975
|
+
[...this.pendingPermissions.values()].some(ownsTurn) ||
|
|
976
|
+
[...this.inFlightPermissions.values()].some(ownsTurn) ||
|
|
977
|
+
[...this.pendingToolPermissions.values()].some(ownsTurn) ||
|
|
978
|
+
[...this.inFlightToolPermissions.values()].some(ownsTurn)
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
clearPendingFreeformSelection(nativeId?: string): void {
|
|
983
|
+
if (nativeId === undefined || this.pendingFreeformSelection?.nativeSelectId === nativeId) {
|
|
984
|
+
this.pendingFreeformSelection = null;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|