@omercnet/paseo-omp 0.4.0-next.124.1 → 0.4.0-next.125.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.
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type {
|
|
3
3
|
ProviderConfigState,
|
|
4
4
|
ProviderError,
|
|
5
5
|
ProviderEvent,
|
|
6
6
|
ProviderInput,
|
|
7
|
-
ProviderPermissionResponse,
|
|
8
7
|
ProviderSessionConfig,
|
|
9
|
-
ProviderToolCallDetail,
|
|
10
8
|
} from "@getpaseo/plugin/server/provider";
|
|
11
9
|
import type {
|
|
12
10
|
OmpOperationalFailure,
|
|
@@ -34,13 +32,10 @@ import type {
|
|
|
34
32
|
OmpAvailableCommand,
|
|
35
33
|
OmpBranchResult,
|
|
36
34
|
OmpCompactionResult,
|
|
37
|
-
OmpExtensionUiResponse,
|
|
38
35
|
OmpMessage,
|
|
39
36
|
OmpModel,
|
|
40
37
|
OmpRpcEvent,
|
|
41
38
|
OmpSessionState,
|
|
42
|
-
OmpToolApprovalCancel,
|
|
43
|
-
OmpToolApprovalRequest,
|
|
44
39
|
} from "./omp-rpc-protocol";
|
|
45
40
|
import { OmpRpcRequestRejectedError } from "./omp-rpc-transport";
|
|
46
41
|
import {
|
|
@@ -66,6 +61,7 @@ import { OmpSessionCompaction } from "./session-compaction";
|
|
|
66
61
|
import type { OmpSessionDescriptor } from "./session-descriptors";
|
|
67
62
|
import { validateNativeSessionId } from "./session-descriptors";
|
|
68
63
|
import { isNativeTurnActivity, isPassiveUiMethod, isRuntimeConfigEvent } from "./session-events";
|
|
64
|
+
import { OmpSessionPermissions } from "./session-permissions";
|
|
69
65
|
import {
|
|
70
66
|
type ActiveTurn,
|
|
71
67
|
assistantTerminalOutcome,
|
|
@@ -96,10 +92,6 @@ type NativeSessionTransition = (previousSessionId: string, nextSessionId: string
|
|
|
96
92
|
type RewindCleanupQuarantine = (cleanup: Promise<void>) => void;
|
|
97
93
|
type RewindSessionRetirement = () => void;
|
|
98
94
|
type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
|
|
99
|
-
type OmpQuestionRequest = Extract<
|
|
100
|
-
Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
|
|
101
|
-
{ method: "select" | "confirm" | "input" | "editor" }
|
|
102
|
-
>;
|
|
103
95
|
type Emit = (event: ProviderEvent) => void;
|
|
104
96
|
const LOCAL_ONLY_SETTLE_MS = 5_000;
|
|
105
97
|
const AGENT_END_STATE_TIMEOUT_MS = 2_000;
|
|
@@ -115,8 +107,8 @@ const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
|
|
|
115
107
|
const MAX_TRACKED_ENTRY_IDS = 1_024;
|
|
116
108
|
const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
|
|
117
109
|
const MAX_PENDING_USERS = 256;
|
|
118
|
-
const
|
|
119
|
-
const
|
|
110
|
+
const _MAX_PENDING_PERMISSIONS = 32;
|
|
111
|
+
const _MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
|
|
120
112
|
const MAX_USER_ECHOES = 512;
|
|
121
113
|
const MAX_BUFFERED_TURN_EVENTS = 512;
|
|
122
114
|
const MAX_BUFFERED_VALUE_ITEMS = 1_024;
|
|
@@ -128,8 +120,8 @@ const MAX_UNCLAIMED_BRANCH_BYTES = 4 * 1024 * 1024;
|
|
|
128
120
|
class OmpCatalogEscape extends OmpPublicError {}
|
|
129
121
|
const MAX_REPLAY_MESSAGES = 100_000;
|
|
130
122
|
const REPLAY_TIMEOUT_MS = 20_000;
|
|
131
|
-
const
|
|
132
|
-
const
|
|
123
|
+
const _OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
|
|
124
|
+
const _MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
|
|
133
125
|
const OMP_BUILTIN_COMMANDS: readonly OmpAvailableCommand[] = [
|
|
134
126
|
{
|
|
135
127
|
name: "compact",
|
|
@@ -255,46 +247,6 @@ type PendingAbort = {
|
|
|
255
247
|
forceTerminal: boolean;
|
|
256
248
|
promise: Promise<void>;
|
|
257
249
|
};
|
|
258
|
-
type PendingPermission = {
|
|
259
|
-
nativeId: string;
|
|
260
|
-
header: string;
|
|
261
|
-
fingerprint: string;
|
|
262
|
-
optionValues: ReadonlyMap<string, string>;
|
|
263
|
-
actionBehaviors: ReadonlyMap<string, "allow" | "deny">;
|
|
264
|
-
retainedBytes: number;
|
|
265
|
-
displayValues: ReadonlyMap<string, string>;
|
|
266
|
-
generation: number;
|
|
267
|
-
runtime: OmpRuntimeSession;
|
|
268
|
-
expiresAt?: number;
|
|
269
|
-
timer?: unknown;
|
|
270
|
-
turnId?: string;
|
|
271
|
-
request: OmpQuestionRequest;
|
|
272
|
-
freeformSentinel?: string;
|
|
273
|
-
};
|
|
274
|
-
type PendingToolPermission = {
|
|
275
|
-
nativeId: string;
|
|
276
|
-
toolCallId: string;
|
|
277
|
-
fingerprint: string;
|
|
278
|
-
retainedBytes: number;
|
|
279
|
-
generation: number;
|
|
280
|
-
runtime: OmpRuntimeSession;
|
|
281
|
-
expiresAt?: number;
|
|
282
|
-
timer?: unknown;
|
|
283
|
-
turnId?: string;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
type PendingFreeformSelection = {
|
|
287
|
-
value: string;
|
|
288
|
-
nativeSelectId: string;
|
|
289
|
-
generation: number;
|
|
290
|
-
runtime: OmpRuntimeSession;
|
|
291
|
-
turnId?: string;
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
function permissionFingerprint(request: OmpQuestionRequest): string {
|
|
295
|
-
return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
|
|
296
|
-
}
|
|
297
|
-
|
|
298
250
|
function providerError(error: unknown, fallback: string): { message: string } {
|
|
299
251
|
return { message: isOmpPublicError(error) ? error.message : fallback };
|
|
300
252
|
}
|
|
@@ -359,14 +311,7 @@ export class OmpProviderSession {
|
|
|
359
311
|
private revertInFlight = false;
|
|
360
312
|
private runtimeTurnCompleted = false;
|
|
361
313
|
private commandCatalog: OmpAvailableCommand[];
|
|
362
|
-
private
|
|
363
|
-
private readonly permissionNamespace = randomUUID();
|
|
364
|
-
private readonly pendingPermissions = new Map<string, PendingPermission>();
|
|
365
|
-
private readonly inFlightPermissions = new Map<string, PendingPermission>();
|
|
366
|
-
private pendingFreeformSelection: PendingFreeformSelection | null = null;
|
|
367
|
-
private readonly pendingToolPermissions = new Map<string, PendingToolPermission>();
|
|
368
|
-
private readonly inFlightToolPermissions = new Map<string, PendingToolPermission>();
|
|
369
|
-
private readonly resolvedToolApprovalIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
|
|
314
|
+
private readonly permissions: OmpSessionPermissions;
|
|
370
315
|
private unsupportedThinkingNoticePending = false;
|
|
371
316
|
private unsupportedThinkingNoticePublished = false;
|
|
372
317
|
|
|
@@ -439,6 +384,24 @@ export class OmpProviderSession {
|
|
|
439
384
|
this.dataFilter,
|
|
440
385
|
this.usage,
|
|
441
386
|
);
|
|
387
|
+
this.permissions = new OmpSessionPermissions({
|
|
388
|
+
sessionId: id,
|
|
389
|
+
emit,
|
|
390
|
+
scheduler,
|
|
391
|
+
dataFilter: this.dataFilter,
|
|
392
|
+
projector: this.projector,
|
|
393
|
+
readContext: () => ({
|
|
394
|
+
runtime: this.runtime,
|
|
395
|
+
generation: this.generation,
|
|
396
|
+
activeTurn: this.activeTurn,
|
|
397
|
+
closed: this.closed,
|
|
398
|
+
runtimeDead: this.runtimeDead !== null,
|
|
399
|
+
}),
|
|
400
|
+
handleRuntimeFailure: (message) => this.handleRuntimeFailure(message),
|
|
401
|
+
invalidateRuntime: (message) => this.invalidateRuntime(message),
|
|
402
|
+
markAgentEvidence: (turn) => this.markAgentEvidence(turn),
|
|
403
|
+
reevaluateDeferredTerminal: () => this.reevaluateDeferredPermissionTerminal(),
|
|
404
|
+
});
|
|
442
405
|
this.subsessions = capabilities.includes("session.subsession")
|
|
443
406
|
? new OmpSubsessionProjector(
|
|
444
407
|
id,
|
|
@@ -1017,7 +980,7 @@ export class OmpProviderSession {
|
|
|
1017
980
|
const configRefresh = this.configRefreshInFlight;
|
|
1018
981
|
this.cancelConfigRefreshRetry();
|
|
1019
982
|
this.lifetime.abort(new Error(message));
|
|
1020
|
-
this.resolveAllPermissions(true);
|
|
983
|
+
this.permissions.resolveAllPermissions(true);
|
|
1021
984
|
this.subsessions?.close();
|
|
1022
985
|
this.projector.close();
|
|
1023
986
|
this.unsubscribe();
|
|
@@ -1265,7 +1228,7 @@ export class OmpProviderSession {
|
|
|
1265
1228
|
turn.steerReady.resolve();
|
|
1266
1229
|
turn.terminal = true;
|
|
1267
1230
|
this.compaction.finish("failed", { message: "OMP prompt failed" });
|
|
1268
|
-
this.resolveTurnPermissions(turn.turnId);
|
|
1231
|
+
this.permissions.resolveTurnPermissions(turn.turnId);
|
|
1269
1232
|
this.projector.finishTurn(turn.turnId);
|
|
1270
1233
|
if (this.activeTurn === turn) this.activeTurn = null;
|
|
1271
1234
|
}
|
|
@@ -1466,7 +1429,7 @@ export class OmpProviderSession {
|
|
|
1466
1429
|
}
|
|
1467
1430
|
const forceTerminal = turn.awaitingPermissionEvidence;
|
|
1468
1431
|
turn.awaitingPermissionEvidence = false;
|
|
1469
|
-
if (forceTerminal) this.resolveTurnPermissions(turn.turnId);
|
|
1432
|
+
if (forceTerminal) this.permissions.resolveTurnPermissions(turn.turnId);
|
|
1470
1433
|
const pending = this.activeAbort;
|
|
1471
1434
|
if (turn.interrupted) {
|
|
1472
1435
|
if (pending?.turn === turn) await this.settleInterrupt(input.requestId, pending);
|
|
@@ -1877,118 +1840,7 @@ export class OmpProviderSession {
|
|
|
1877
1840
|
}
|
|
1878
1841
|
|
|
1879
1842
|
async permission(input: SessionPermissionInput): Promise<void> {
|
|
1880
|
-
|
|
1881
|
-
if (typed) {
|
|
1882
|
-
await this.respondToToolPermission(input, typed);
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
const pending = this.pendingPermissions.get(input.permissionId);
|
|
1886
|
-
if (!pending) throw new OmpPublicError("Unknown OMP permission request");
|
|
1887
|
-
if (!this.permissionOwnerIsCurrent(pending)) {
|
|
1888
|
-
this.pendingPermissions.delete(input.permissionId);
|
|
1889
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
1890
|
-
this.emit({
|
|
1891
|
-
type: "session.permission_resolved",
|
|
1892
|
-
sessionId: this.id,
|
|
1893
|
-
permissionId: input.permissionId,
|
|
1894
|
-
});
|
|
1895
|
-
throw new OmpPublicError("OMP permission request is no longer active");
|
|
1896
|
-
}
|
|
1897
|
-
const { generation, runtime } = pending;
|
|
1898
|
-
this.pendingPermissions.delete(input.permissionId);
|
|
1899
|
-
this.inFlightPermissions.set(input.permissionId, pending);
|
|
1900
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
1901
|
-
try {
|
|
1902
|
-
const freeformValue = this.freeformSelection(pending, input.response);
|
|
1903
|
-
if (freeformValue !== undefined) {
|
|
1904
|
-
this.pendingFreeformSelection = {
|
|
1905
|
-
value: freeformValue,
|
|
1906
|
-
nativeSelectId: pending.nativeId,
|
|
1907
|
-
generation,
|
|
1908
|
-
runtime,
|
|
1909
|
-
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
1910
|
-
};
|
|
1911
|
-
}
|
|
1912
|
-
const response = this.extensionUiResponse(pending, input.response);
|
|
1913
|
-
await runtime.respondToExtensionUi(response);
|
|
1914
|
-
} catch (error) {
|
|
1915
|
-
if (this.pendingFreeformSelection?.nativeSelectId === pending.nativeId) {
|
|
1916
|
-
this.pendingFreeformSelection = null;
|
|
1917
|
-
}
|
|
1918
|
-
if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
|
|
1919
|
-
this.inFlightPermissions.delete(input.permissionId);
|
|
1920
|
-
if (
|
|
1921
|
-
this.permissionOwnerIsCurrent(pending) &&
|
|
1922
|
-
generation === this.generation &&
|
|
1923
|
-
runtime === this.runtime
|
|
1924
|
-
) {
|
|
1925
|
-
this.pendingPermissions.set(input.permissionId, pending);
|
|
1926
|
-
this.armPermissionTimeout(input.permissionId, pending);
|
|
1927
|
-
throw error;
|
|
1928
|
-
}
|
|
1929
|
-
return;
|
|
1930
|
-
}
|
|
1931
|
-
if (this.inFlightPermissions.get(input.permissionId) !== pending) return;
|
|
1932
|
-
this.inFlightPermissions.delete(input.permissionId);
|
|
1933
|
-
this.emit({
|
|
1934
|
-
type: "session.permission_resolved",
|
|
1935
|
-
sessionId: this.id,
|
|
1936
|
-
permissionId: input.permissionId,
|
|
1937
|
-
});
|
|
1938
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
1939
|
-
}
|
|
1940
|
-
private async respondToToolPermission(
|
|
1941
|
-
input: SessionPermissionInput,
|
|
1942
|
-
pending: PendingToolPermission,
|
|
1943
|
-
): Promise<void> {
|
|
1944
|
-
if (!this.permissionOwnerIsCurrent(pending)) {
|
|
1945
|
-
this.pendingToolPermissions.delete(input.permissionId);
|
|
1946
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
1947
|
-
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
1948
|
-
this.emit({
|
|
1949
|
-
type: "session.permission_resolved",
|
|
1950
|
-
sessionId: this.id,
|
|
1951
|
-
permissionId: input.permissionId,
|
|
1952
|
-
});
|
|
1953
|
-
throw new OmpPublicError("OMP permission request is no longer active");
|
|
1954
|
-
}
|
|
1955
|
-
if (
|
|
1956
|
-
input.response.selectedActionId !== undefined &&
|
|
1957
|
-
input.response.selectedActionId !== input.response.behavior
|
|
1958
|
-
) {
|
|
1959
|
-
throw new OmpPublicError("OMP permission action is invalid");
|
|
1960
|
-
}
|
|
1961
|
-
this.pendingToolPermissions.delete(input.permissionId);
|
|
1962
|
-
this.inFlightToolPermissions.set(input.permissionId, pending);
|
|
1963
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
1964
|
-
try {
|
|
1965
|
-
await pending.runtime.respondToToolApproval({
|
|
1966
|
-
type: "tool_approval_response",
|
|
1967
|
-
id: pending.nativeId,
|
|
1968
|
-
toolCallId: pending.toolCallId,
|
|
1969
|
-
approved: input.response.behavior === "allow",
|
|
1970
|
-
});
|
|
1971
|
-
} catch (error) {
|
|
1972
|
-
if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
|
|
1973
|
-
this.inFlightToolPermissions.delete(input.permissionId);
|
|
1974
|
-
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
1975
|
-
this.emit({
|
|
1976
|
-
type: "session.permission_resolved",
|
|
1977
|
-
sessionId: this.id,
|
|
1978
|
-
permissionId: input.permissionId,
|
|
1979
|
-
});
|
|
1980
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
1981
|
-
throw error;
|
|
1982
|
-
}
|
|
1983
|
-
if (this.inFlightToolPermissions.get(input.permissionId) !== pending) return;
|
|
1984
|
-
this.inFlightToolPermissions.delete(input.permissionId);
|
|
1985
|
-
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
1986
|
-
this.emit({
|
|
1987
|
-
type: "session.permission_resolved",
|
|
1988
|
-
sessionId: this.id,
|
|
1989
|
-
permissionId: input.permissionId,
|
|
1990
|
-
});
|
|
1991
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
1843
|
+
await this.permissions.respond(input);
|
|
1992
1844
|
}
|
|
1993
1845
|
|
|
1994
1846
|
close(input?: SessionCloseInput): Promise<void> {
|
|
@@ -2027,7 +1879,7 @@ export class OmpProviderSession {
|
|
|
2027
1879
|
}
|
|
2028
1880
|
this.imageMaterializer.clear();
|
|
2029
1881
|
this.compaction.finish("canceled");
|
|
2030
|
-
this.resolveAllPermissions(true);
|
|
1882
|
+
this.permissions.resolveAllPermissions(true);
|
|
2031
1883
|
this.closed = true;
|
|
2032
1884
|
this.configRefreshAttempts = 0;
|
|
2033
1885
|
this.configRefreshDirty = false;
|
|
@@ -2352,7 +2204,7 @@ export class OmpProviderSession {
|
|
|
2352
2204
|
this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval request");
|
|
2353
2205
|
return;
|
|
2354
2206
|
}
|
|
2355
|
-
this.publishToolPermission(event);
|
|
2207
|
+
this.permissions.publishToolPermission(event);
|
|
2356
2208
|
return;
|
|
2357
2209
|
}
|
|
2358
2210
|
if (event.type === "tool_approval_cancel") {
|
|
@@ -2360,18 +2212,17 @@ export class OmpProviderSession {
|
|
|
2360
2212
|
this.handleRuntimeFailure("OMP emitted an unnegotiated tool approval cancellation");
|
|
2361
2213
|
return;
|
|
2362
2214
|
}
|
|
2363
|
-
this.cancelToolPermission(event);
|
|
2215
|
+
this.permissions.cancelToolPermission(event);
|
|
2364
2216
|
return;
|
|
2365
2217
|
}
|
|
2366
2218
|
if (event.type === "extension_ui_request") {
|
|
2367
2219
|
if (event.method === "cancel") {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
}
|
|
2371
|
-
this.resolvePermissionByNativeId(event.targetId ?? "");
|
|
2220
|
+
this.permissions.clearPendingFreeformSelection(event.targetId);
|
|
2221
|
+
this.permissions.resolvePermissionByNativeId(event.targetId ?? "");
|
|
2372
2222
|
return;
|
|
2373
2223
|
}
|
|
2374
|
-
if (event.method === "input" && this.submitPendingFreeformSelection(event))
|
|
2224
|
+
if (event.method === "input" && this.permissions.submitPendingFreeformSelection(event))
|
|
2225
|
+
return;
|
|
2375
2226
|
if (
|
|
2376
2227
|
event.method === "select" ||
|
|
2377
2228
|
event.method === "confirm" ||
|
|
@@ -2382,7 +2233,7 @@ export class OmpProviderSession {
|
|
|
2382
2233
|
this.handleRuntimeFailure();
|
|
2383
2234
|
return;
|
|
2384
2235
|
}
|
|
2385
|
-
this.publishPermission(event);
|
|
2236
|
+
this.permissions.publishPermission(event);
|
|
2386
2237
|
return;
|
|
2387
2238
|
}
|
|
2388
2239
|
|
|
@@ -2546,7 +2397,7 @@ export class OmpProviderSession {
|
|
|
2546
2397
|
if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
|
|
2547
2398
|
if (event.type === "tool_execution_end") {
|
|
2548
2399
|
turn.activeToolCallIds.delete(event.toolCallId);
|
|
2549
|
-
if (event.toolName === "ask_user") this.
|
|
2400
|
+
if (event.toolName === "ask_user") this.permissions.clearPendingFreeformSelection();
|
|
2550
2401
|
}
|
|
2551
2402
|
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
2552
2403
|
try {
|
|
@@ -2821,24 +2672,6 @@ export class OmpProviderSession {
|
|
|
2821
2672
|
});
|
|
2822
2673
|
}
|
|
2823
2674
|
|
|
2824
|
-
private submitPendingFreeformSelection(
|
|
2825
|
-
request: Extract<OmpQuestionRequest, { method: "input" }>,
|
|
2826
|
-
): boolean {
|
|
2827
|
-
const pending = this.pendingFreeformSelection;
|
|
2828
|
-
if (!pending) return false;
|
|
2829
|
-
this.pendingFreeformSelection = null;
|
|
2830
|
-
if (
|
|
2831
|
-
pending.generation !== this.generation ||
|
|
2832
|
-
pending.runtime !== this.runtime ||
|
|
2833
|
-
(pending.turnId !== undefined && this.activeTurn?.turnId !== pending.turnId)
|
|
2834
|
-
) {
|
|
2835
|
-
return false;
|
|
2836
|
-
}
|
|
2837
|
-
void pending.runtime
|
|
2838
|
-
.respondToExtensionUi({ type: "extension_ui_response", id: request.id, value: pending.value })
|
|
2839
|
-
.catch(() => this.handleRuntimeFailure("OMP freeform response failed"));
|
|
2840
|
-
return true;
|
|
2841
|
-
}
|
|
2842
2675
|
private replaceSlashCommands(commands: OmpAvailableCommand[]): void {
|
|
2843
2676
|
this.slashCommands.clear();
|
|
2844
2677
|
for (const command of [...OMP_BUILTIN_COMMANDS, ...commands]) {
|
|
@@ -2870,687 +2703,11 @@ export class OmpProviderSession {
|
|
|
2870
2703
|
}),
|
|
2871
2704
|
});
|
|
2872
2705
|
}
|
|
2873
|
-
private toolPermissionDetail(
|
|
2874
|
-
request: OmpToolApprovalRequest,
|
|
2875
|
-
): ProviderToolCallDetail | undefined {
|
|
2876
|
-
switch (request.identity.kind) {
|
|
2877
|
-
case "shell":
|
|
2878
|
-
return {
|
|
2879
|
-
type: "shell",
|
|
2880
|
-
command: this.dataFilter.text(request.identity.command, 24 * 1024),
|
|
2881
|
-
};
|
|
2882
|
-
case "edit":
|
|
2883
|
-
return {
|
|
2884
|
-
type: "edit",
|
|
2885
|
-
filePath: this.dataFilter.text(request.identity.paths[0] ?? "", 4_096),
|
|
2886
|
-
newString: this.dataFilter.text(request.identity.content, 20 * 1024),
|
|
2887
|
-
};
|
|
2888
|
-
case "write":
|
|
2889
|
-
return {
|
|
2890
|
-
type: "write",
|
|
2891
|
-
filePath: this.dataFilter.text(request.identity.path, 4_096),
|
|
2892
|
-
content: this.dataFilter.text(request.identity.content, 20 * 1024),
|
|
2893
|
-
};
|
|
2894
|
-
case "other":
|
|
2895
|
-
return;
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
private publishToolPermission(request: OmpToolApprovalRequest): void {
|
|
2900
|
-
const fingerprint = createHash("sha256").update(JSON.stringify(request)).digest("base64url");
|
|
2901
|
-
if (this.resolvedToolApprovalIds.has(request.id)) {
|
|
2902
|
-
this.handleRuntimeFailure("OMP reused a resolved tool approval identifier");
|
|
2903
|
-
return;
|
|
2904
|
-
}
|
|
2905
|
-
for (const pending of [
|
|
2906
|
-
...this.pendingToolPermissions.values(),
|
|
2907
|
-
...this.inFlightToolPermissions.values(),
|
|
2908
|
-
]) {
|
|
2909
|
-
if (pending.nativeId !== request.id) continue;
|
|
2910
|
-
if (pending.fingerprint !== fingerprint || pending.toolCallId !== request.toolCallId) {
|
|
2911
|
-
this.handleRuntimeFailure("OMP changed a pending tool approval request");
|
|
2912
|
-
}
|
|
2913
|
-
return;
|
|
2914
|
-
}
|
|
2915
|
-
const pendingCount =
|
|
2916
|
-
this.pendingPermissions.size +
|
|
2917
|
-
this.inFlightPermissions.size +
|
|
2918
|
-
this.pendingToolPermissions.size +
|
|
2919
|
-
this.inFlightToolPermissions.size;
|
|
2920
|
-
if (pendingCount >= MAX_PENDING_PERMISSIONS) {
|
|
2921
|
-
this.resolvedToolApprovalIds.add(request.id);
|
|
2922
|
-
void this.runtime
|
|
2923
|
-
.respondToToolApproval({
|
|
2924
|
-
type: "tool_approval_response",
|
|
2925
|
-
id: request.id,
|
|
2926
|
-
toolCallId: request.toolCallId,
|
|
2927
|
-
cancelled: true,
|
|
2928
|
-
})
|
|
2929
|
-
.catch(() => this.handleRuntimeFailure());
|
|
2930
|
-
return;
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
this.permissionSequence += 1;
|
|
2934
|
-
const permissionId = `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
|
|
2935
|
-
const detail = this.toolPermissionDetail(request);
|
|
2936
|
-
const filteredInput = this.dataFilter.json(request.input, 8 * 1024, 32 * 1024);
|
|
2937
|
-
const input =
|
|
2938
|
-
filteredInput && typeof filteredInput === "object" && !Array.isArray(filteredInput)
|
|
2939
|
-
? filteredInput
|
|
2940
|
-
: {};
|
|
2941
|
-
const descriptionParts = [
|
|
2942
|
-
request.detail.reason,
|
|
2943
|
-
...request.detail.lines,
|
|
2944
|
-
...(request.detail.providerSafetyChecks ?? []),
|
|
2945
|
-
].filter((part): part is string => Boolean(part));
|
|
2946
|
-
const publicRequest = {
|
|
2947
|
-
id: permissionId,
|
|
2948
|
-
name: `omp.${this.dataFilter.text(request.toolName, 256)}`,
|
|
2949
|
-
kind: "tool" as const,
|
|
2950
|
-
title: `Allow ${this.dataFilter.text(request.toolName, 256)}?`,
|
|
2951
|
-
...(descriptionParts.length > 0
|
|
2952
|
-
? { description: this.dataFilter.text(descriptionParts.join("\n"), 16 * 1024) }
|
|
2953
|
-
: {}),
|
|
2954
|
-
input: {
|
|
2955
|
-
...input,
|
|
2956
|
-
identity: this.dataFilter.json(request.identity, 20 * 1024, 32 * 1024),
|
|
2957
|
-
tier: request.tier,
|
|
2958
|
-
toolCallId: request.toolCallId,
|
|
2959
|
-
},
|
|
2960
|
-
...(detail ? { detail } : {}),
|
|
2961
|
-
actions: [
|
|
2962
|
-
{ id: "allow", label: "Allow", behavior: "allow" as const, variant: "primary" as const },
|
|
2963
|
-
{ id: "deny", label: "Deny", behavior: "deny" as const, variant: "danger" as const },
|
|
2964
|
-
],
|
|
2965
|
-
metadata: {
|
|
2966
|
-
tier: request.tier,
|
|
2967
|
-
redacted: request.detail.redacted,
|
|
2968
|
-
truncated: request.detail.truncated,
|
|
2969
|
-
redactedFields: request.detail.redactedFields,
|
|
2970
|
-
truncatedFields: request.detail.truncatedFields,
|
|
2971
|
-
},
|
|
2972
|
-
};
|
|
2973
|
-
const pending: PendingToolPermission = {
|
|
2974
|
-
nativeId: request.id,
|
|
2975
|
-
toolCallId: request.toolCallId,
|
|
2976
|
-
fingerprint,
|
|
2977
|
-
retainedBytes: boundedJsonBytes(
|
|
2978
|
-
publicRequest,
|
|
2979
|
-
MAX_PENDING_PERMISSION_BYTES,
|
|
2980
|
-
512,
|
|
2981
|
-
MAX_PENDING_PERMISSION_BYTES,
|
|
2982
|
-
4_096,
|
|
2983
|
-
),
|
|
2984
|
-
generation: this.generation,
|
|
2985
|
-
runtime: this.runtime,
|
|
2986
|
-
...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
|
|
2987
|
-
...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
|
|
2988
|
-
};
|
|
2989
|
-
let retainedBytes = pending.retainedBytes;
|
|
2990
|
-
for (const item of this.pendingPermissions.values()) retainedBytes += item.retainedBytes;
|
|
2991
|
-
for (const item of this.inFlightPermissions.values()) retainedBytes += item.retainedBytes;
|
|
2992
|
-
for (const item of this.pendingToolPermissions.values()) retainedBytes += item.retainedBytes;
|
|
2993
|
-
for (const item of this.inFlightToolPermissions.values()) retainedBytes += item.retainedBytes;
|
|
2994
|
-
if (
|
|
2995
|
-
pending.retainedBytes === Number.POSITIVE_INFINITY ||
|
|
2996
|
-
retainedBytes > MAX_PENDING_PERMISSION_BYTES
|
|
2997
|
-
) {
|
|
2998
|
-
void this.runtime
|
|
2999
|
-
.respondToToolApproval({
|
|
3000
|
-
type: "tool_approval_response",
|
|
3001
|
-
id: request.id,
|
|
3002
|
-
toolCallId: request.toolCallId,
|
|
3003
|
-
cancelled: true,
|
|
3004
|
-
})
|
|
3005
|
-
.catch(() => this.handleRuntimeFailure());
|
|
3006
|
-
this.resolvedToolApprovalIds.add(request.id);
|
|
3007
|
-
return;
|
|
3008
|
-
}
|
|
3009
|
-
this.pendingToolPermissions.set(permissionId, pending);
|
|
3010
|
-
this.armToolPermissionTimeout(permissionId, pending);
|
|
3011
|
-
this.projector.markAskPermissionRendered();
|
|
3012
|
-
if (this.activeTurn) {
|
|
3013
|
-
this.activeTurn.awaitingPermissionEvidence = true;
|
|
3014
|
-
this.markAgentEvidence(this.activeTurn);
|
|
3015
|
-
}
|
|
3016
|
-
this.emit({ type: "session.permission", sessionId: this.id, request: publicRequest });
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
private cancelToolPermission(request: OmpToolApprovalCancel): void {
|
|
3020
|
-
for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
|
|
3021
|
-
for (const [permissionId, pending] of permissions) {
|
|
3022
|
-
if (pending.nativeId !== request.targetId) continue;
|
|
3023
|
-
if (pending.toolCallId !== request.toolCallId) {
|
|
3024
|
-
this.handleRuntimeFailure("OMP tool approval cancellation did not match its tool call");
|
|
3025
|
-
return;
|
|
3026
|
-
}
|
|
3027
|
-
permissions.delete(permissionId);
|
|
3028
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
3029
|
-
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
3030
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3031
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3032
|
-
return;
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
if (!this.resolvedToolApprovalIds.has(request.targetId)) {
|
|
3036
|
-
this.handleRuntimeFailure("OMP canceled an unknown tool approval request");
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
|
-
private publishPermission(request: OmpQuestionRequest): void {
|
|
3041
|
-
if (request.method === "select" && !request.options?.length) {
|
|
3042
|
-
this.handleRuntimeFailure();
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
const fingerprint = permissionFingerprint(request);
|
|
3046
|
-
let existingId: string | undefined;
|
|
3047
|
-
let existingPending: PendingPermission | undefined;
|
|
3048
|
-
for (const [permissionId, pending] of this.pendingPermissions) {
|
|
3049
|
-
if (pending.nativeId !== request.id) continue;
|
|
3050
|
-
if (pending.fingerprint === fingerprint) {
|
|
3051
|
-
existingId = permissionId;
|
|
3052
|
-
existingPending = pending;
|
|
3053
|
-
} else {
|
|
3054
|
-
this.pendingPermissions.delete(permissionId);
|
|
3055
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
3056
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3057
|
-
}
|
|
3058
|
-
break;
|
|
3059
|
-
}
|
|
3060
|
-
if (!existingId) {
|
|
3061
|
-
for (const pending of this.inFlightPermissions.values()) {
|
|
3062
|
-
if (pending.nativeId !== request.id) continue;
|
|
3063
|
-
if (pending.fingerprint !== fingerprint) this.handleRuntimeFailure();
|
|
3064
|
-
return;
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
if (
|
|
3068
|
-
!existingId &&
|
|
3069
|
-
this.pendingPermissions.size +
|
|
3070
|
-
this.inFlightPermissions.size +
|
|
3071
|
-
this.pendingToolPermissions.size +
|
|
3072
|
-
this.inFlightToolPermissions.size >=
|
|
3073
|
-
MAX_PENDING_PERMISSIONS
|
|
3074
|
-
) {
|
|
3075
|
-
this.rejectPermissionRequest(request, "Too many OMP questions are already pending");
|
|
3076
|
-
return;
|
|
3077
|
-
}
|
|
3078
|
-
if (existingPending?.timer !== undefined) this.scheduler.clear(existingPending.timer);
|
|
3079
|
-
if (!existingId) this.permissionSequence += 1;
|
|
3080
|
-
const id =
|
|
3081
|
-
existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
|
|
3082
|
-
const header = this.dataFilter.text(request.title ?? "OMP question", 4_096);
|
|
3083
|
-
const freeformSentinel =
|
|
3084
|
-
request.method === "select" && request.options.includes(OMP_ASK_USER_FREEFORM_SENTINEL)
|
|
3085
|
-
? OMP_ASK_USER_FREEFORM_SENTINEL
|
|
3086
|
-
: undefined;
|
|
3087
|
-
const optionValues = new Map<string, string>();
|
|
3088
|
-
const displayValues = new Map<string, string>();
|
|
3089
|
-
const usedOptionLabels = new Set<string>();
|
|
3090
|
-
const optionDetails = request.method === "select" ? request.optionDetails : undefined;
|
|
3091
|
-
const selectableOptions =
|
|
3092
|
-
request.method === "select"
|
|
3093
|
-
? request.options.flatMap((nativeValue, index) =>
|
|
3094
|
-
nativeValue === freeformSentinel ? [] : [{ nativeValue, index }],
|
|
3095
|
-
)
|
|
3096
|
-
: [];
|
|
3097
|
-
const options =
|
|
3098
|
-
request.method === "select"
|
|
3099
|
-
? selectableOptions.map(({ nativeValue, index }) => {
|
|
3100
|
-
const baseLabel = this.dataFilter.text(nativeValue, 4_096);
|
|
3101
|
-
let label = baseLabel;
|
|
3102
|
-
let suffix = 2;
|
|
3103
|
-
while (usedOptionLabels.has(label)) {
|
|
3104
|
-
label = `${baseLabel} (${suffix})`;
|
|
3105
|
-
suffix += 1;
|
|
3106
|
-
}
|
|
3107
|
-
usedOptionLabels.add(label);
|
|
3108
|
-
const value = `${id}:option:${index}`;
|
|
3109
|
-
optionValues.set(value, nativeValue);
|
|
3110
|
-
displayValues.set(label, nativeValue);
|
|
3111
|
-
return {
|
|
3112
|
-
label,
|
|
3113
|
-
value,
|
|
3114
|
-
...(optionDetails?.[index]?.description
|
|
3115
|
-
? {
|
|
3116
|
-
description: this.dataFilter.text(
|
|
3117
|
-
optionDetails[index]?.description ?? "",
|
|
3118
|
-
16_384,
|
|
3119
|
-
),
|
|
3120
|
-
}
|
|
3121
|
-
: {}),
|
|
3122
|
-
};
|
|
3123
|
-
})
|
|
3124
|
-
: undefined;
|
|
3125
|
-
const questionOptions =
|
|
3126
|
-
request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
|
|
3127
|
-
const actions =
|
|
3128
|
-
request.method === "select"
|
|
3129
|
-
? [
|
|
3130
|
-
...(options ?? []).map((option) => ({
|
|
3131
|
-
id: option.value,
|
|
3132
|
-
label: option.label,
|
|
3133
|
-
behavior: "allow" as const,
|
|
3134
|
-
variant: "secondary" as const,
|
|
3135
|
-
})),
|
|
3136
|
-
{
|
|
3137
|
-
id: "cancel",
|
|
3138
|
-
label: "Cancel",
|
|
3139
|
-
behavior: "deny" as const,
|
|
3140
|
-
variant: "secondary" as const,
|
|
3141
|
-
},
|
|
3142
|
-
]
|
|
3143
|
-
: [
|
|
3144
|
-
{
|
|
3145
|
-
id: "submit",
|
|
3146
|
-
label: request.method === "confirm" ? "Confirm" : "Submit",
|
|
3147
|
-
behavior: "allow" as const,
|
|
3148
|
-
variant: "primary" as const,
|
|
3149
|
-
},
|
|
3150
|
-
{
|
|
3151
|
-
id: "cancel",
|
|
3152
|
-
label: "Cancel",
|
|
3153
|
-
behavior: "deny" as const,
|
|
3154
|
-
variant: "secondary" as const,
|
|
3155
|
-
},
|
|
3156
|
-
];
|
|
3157
|
-
const pending: PendingPermission = {
|
|
3158
|
-
nativeId: request.id,
|
|
3159
|
-
header,
|
|
3160
|
-
fingerprint,
|
|
3161
|
-
optionValues,
|
|
3162
|
-
actionBehaviors: new Map(actions.map((action) => [action.id, action.behavior])),
|
|
3163
|
-
displayValues,
|
|
3164
|
-
generation: this.generation,
|
|
3165
|
-
runtime: this.runtime,
|
|
3166
|
-
request,
|
|
3167
|
-
retainedBytes: boundedJsonBytes(
|
|
3168
|
-
{ request, header, options: questionOptions, actions },
|
|
3169
|
-
MAX_PENDING_PERMISSION_BYTES,
|
|
3170
|
-
512,
|
|
3171
|
-
MAX_PENDING_PERMISSION_BYTES,
|
|
3172
|
-
4_096,
|
|
3173
|
-
),
|
|
3174
|
-
...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
|
|
3175
|
-
...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
|
|
3176
|
-
...(freeformSentinel ? { freeformSentinel } : {}),
|
|
3177
|
-
};
|
|
3178
|
-
let retainedPermissionBytes = pending.retainedBytes;
|
|
3179
|
-
for (const [permissionId, retained] of this.pendingPermissions) {
|
|
3180
|
-
if (permissionId !== existingId) retainedPermissionBytes += retained.retainedBytes;
|
|
3181
|
-
}
|
|
3182
|
-
for (const retained of this.inFlightPermissions.values()) {
|
|
3183
|
-
retainedPermissionBytes += retained.retainedBytes;
|
|
3184
|
-
}
|
|
3185
|
-
for (const retained of this.pendingToolPermissions.values()) {
|
|
3186
|
-
retainedPermissionBytes += retained.retainedBytes;
|
|
3187
|
-
}
|
|
3188
|
-
for (const retained of this.inFlightToolPermissions.values()) {
|
|
3189
|
-
retainedPermissionBytes += retained.retainedBytes;
|
|
3190
|
-
}
|
|
3191
|
-
if (
|
|
3192
|
-
pending.retainedBytes === Number.POSITIVE_INFINITY ||
|
|
3193
|
-
retainedPermissionBytes > MAX_PENDING_PERMISSION_BYTES
|
|
3194
|
-
) {
|
|
3195
|
-
if (existingId) {
|
|
3196
|
-
this.pendingPermissions.delete(existingId);
|
|
3197
|
-
this.emit({
|
|
3198
|
-
type: "session.permission_resolved",
|
|
3199
|
-
sessionId: this.id,
|
|
3200
|
-
permissionId: existingId,
|
|
3201
|
-
});
|
|
3202
|
-
}
|
|
3203
|
-
this.rejectPermissionRequest(request, "OMP question data exceeded the pending input budget");
|
|
3204
|
-
return;
|
|
3205
|
-
}
|
|
3206
|
-
this.pendingPermissions.set(id, pending);
|
|
3207
|
-
this.armPermissionTimeout(id, pending);
|
|
3208
|
-
this.projector.markAskPermissionRendered();
|
|
3209
|
-
if (this.activeTurn) {
|
|
3210
|
-
this.activeTurn.awaitingPermissionEvidence = true;
|
|
3211
|
-
this.markAgentEvidence(this.activeTurn);
|
|
3212
|
-
}
|
|
3213
|
-
this.emit({
|
|
3214
|
-
type: "session.permission",
|
|
3215
|
-
sessionId: this.id,
|
|
3216
|
-
request: {
|
|
3217
|
-
id,
|
|
3218
|
-
name: `omp.${request.method}`,
|
|
3219
|
-
kind: "question",
|
|
3220
|
-
title: header,
|
|
3221
|
-
...(request.method === "confirm"
|
|
3222
|
-
? { description: this.dataFilter.text(request.message, 64 * 1024) }
|
|
3223
|
-
: {}),
|
|
3224
|
-
input: {
|
|
3225
|
-
questions: [
|
|
3226
|
-
{
|
|
3227
|
-
header,
|
|
3228
|
-
question: this.dataFilter.text(
|
|
3229
|
-
request.method === "confirm" ? request.message : request.title,
|
|
3230
|
-
64 * 1024,
|
|
3231
|
-
),
|
|
3232
|
-
options: questionOptions,
|
|
3233
|
-
multiSelect: false,
|
|
3234
|
-
...(freeformSentinel ? { allowOther: true } : {}),
|
|
3235
|
-
...(request.method === "input" && request.placeholder
|
|
3236
|
-
? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
|
|
3237
|
-
: {}),
|
|
3238
|
-
...((request.method === "input" || request.method === "editor") && request.prefill
|
|
3239
|
-
? { prefill: this.dataFilter.text(request.prefill) }
|
|
3240
|
-
: {}),
|
|
3241
|
-
},
|
|
3242
|
-
],
|
|
3243
|
-
},
|
|
3244
|
-
actions,
|
|
3245
|
-
},
|
|
3246
|
-
});
|
|
3247
|
-
}
|
|
3248
|
-
|
|
3249
|
-
private rejectPermissionRequest(request: OmpQuestionRequest, description: string): void {
|
|
3250
|
-
this.emit({
|
|
3251
|
-
type: "session.notice",
|
|
3252
|
-
sessionId: this.id,
|
|
3253
|
-
notice: {
|
|
3254
|
-
id: `omp:permission-rejected:${this.permissionSequence + 1}`,
|
|
3255
|
-
severity: "warning",
|
|
3256
|
-
title: "OMP question canceled",
|
|
3257
|
-
description,
|
|
3258
|
-
},
|
|
3259
|
-
});
|
|
3260
|
-
void this.runtime
|
|
3261
|
-
.respondToExtensionUi({ type: "extension_ui_response", id: request.id, cancelled: true })
|
|
3262
|
-
.catch(() => this.handleRuntimeFailure());
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
|
-
private freeformSelection(
|
|
3266
|
-
pending: PendingPermission,
|
|
3267
|
-
response: ProviderPermissionResponse,
|
|
3268
|
-
): string | undefined {
|
|
3269
|
-
if (
|
|
3270
|
-
pending.request.method !== "select" ||
|
|
3271
|
-
!pending.freeformSentinel ||
|
|
3272
|
-
response.behavior !== "allow" ||
|
|
3273
|
-
response.selectedActionId !== undefined
|
|
3274
|
-
) {
|
|
3275
|
-
return;
|
|
3276
|
-
}
|
|
3277
|
-
const answers = response.updatedInput?.answers;
|
|
3278
|
-
const answer =
|
|
3279
|
-
answers && typeof answers === "object" && !Array.isArray(answers)
|
|
3280
|
-
? answers[pending.header]
|
|
3281
|
-
: undefined;
|
|
3282
|
-
const value = Array.isArray(answer) ? answer[0] : answer;
|
|
3283
|
-
if (
|
|
3284
|
-
typeof value !== "string" ||
|
|
3285
|
-
pending.optionValues.has(value) ||
|
|
3286
|
-
pending.displayValues.has(value)
|
|
3287
|
-
) {
|
|
3288
|
-
return;
|
|
3289
|
-
}
|
|
3290
|
-
if (
|
|
3291
|
-
value === pending.freeformSentinel ||
|
|
3292
|
-
value.trim().length === 0 ||
|
|
3293
|
-
value.includes("\0") ||
|
|
3294
|
-
utf8Bytes(value) > MAX_FREEFORM_RESPONSE_BYTES
|
|
3295
|
-
) {
|
|
3296
|
-
throw new OmpPublicError("OMP freeform response is invalid");
|
|
3297
|
-
}
|
|
3298
|
-
return value;
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
|
-
private extensionUiResponse(
|
|
3302
|
-
pending: PendingPermission,
|
|
3303
|
-
response: ProviderPermissionResponse,
|
|
3304
|
-
): OmpExtensionUiResponse {
|
|
3305
|
-
if (response.selectedActionId !== undefined) {
|
|
3306
|
-
const expectedBehavior = pending.actionBehaviors.get(response.selectedActionId);
|
|
3307
|
-
if (expectedBehavior === undefined || expectedBehavior !== response.behavior) {
|
|
3308
|
-
throw new OmpPublicError("OMP permission action is invalid");
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
const { nativeId, request, header } = pending;
|
|
3312
|
-
if (request.method === "confirm") {
|
|
3313
|
-
if (response.behavior === "deny") {
|
|
3314
|
-
return { type: "extension_ui_response", id: nativeId, confirmed: false };
|
|
3315
|
-
}
|
|
3316
|
-
const answers = response.updatedInput?.answers;
|
|
3317
|
-
const answer =
|
|
3318
|
-
answers && typeof answers === "object" && !Array.isArray(answers)
|
|
3319
|
-
? answers[header]
|
|
3320
|
-
: undefined;
|
|
3321
|
-
return {
|
|
3322
|
-
type: "extension_ui_response",
|
|
3323
|
-
id: nativeId,
|
|
3324
|
-
confirmed: typeof answer === "string" ? /^yes$/iu.test(answer.trim()) : true,
|
|
3325
|
-
};
|
|
3326
|
-
}
|
|
3327
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3328
|
-
if (response.behavior === "deny") {
|
|
3329
|
-
return { type: "extension_ui_response", id: nativeId, cancelled: true };
|
|
3330
|
-
}
|
|
3331
|
-
const selectedValue = response.selectedActionId
|
|
3332
|
-
? pending.optionValues.get(response.selectedActionId)
|
|
3333
|
-
: undefined;
|
|
3334
|
-
if (selectedValue !== undefined) {
|
|
3335
|
-
return { type: "extension_ui_response", id: nativeId, value: selectedValue };
|
|
3336
|
-
}
|
|
3337
|
-
const answers = response.updatedInput?.answers;
|
|
3338
|
-
if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
|
|
3339
|
-
throw new OmpPublicError("OMP question response requires answers");
|
|
3340
|
-
}
|
|
3341
|
-
const answer = answers[header];
|
|
3342
|
-
const publicValue = Array.isArray(answer) ? answer[0] : answer;
|
|
3343
|
-
if (typeof publicValue !== "string") {
|
|
3344
|
-
throw new OmpPublicError("OMP question response is invalid");
|
|
3345
|
-
}
|
|
3346
|
-
if (
|
|
3347
|
-
request.method === "select" &&
|
|
3348
|
-
pending.freeformSentinel &&
|
|
3349
|
-
publicValue !== pending.freeformSentinel &&
|
|
3350
|
-
!pending.displayValues.has(publicValue) &&
|
|
3351
|
-
!pending.optionValues.has(publicValue)
|
|
3352
|
-
) {
|
|
3353
|
-
if (
|
|
3354
|
-
publicValue.trim().length === 0 ||
|
|
3355
|
-
publicValue.includes("\0") ||
|
|
3356
|
-
utf8Bytes(publicValue) > MAX_FREEFORM_RESPONSE_BYTES
|
|
3357
|
-
) {
|
|
3358
|
-
throw new OmpPublicError("OMP freeform response is invalid");
|
|
3359
|
-
}
|
|
3360
|
-
this.pendingFreeformSelection = {
|
|
3361
|
-
value: publicValue,
|
|
3362
|
-
nativeSelectId: pending.nativeId,
|
|
3363
|
-
generation: pending.generation,
|
|
3364
|
-
runtime: pending.runtime,
|
|
3365
|
-
...(pending.turnId ? { turnId: pending.turnId } : {}),
|
|
3366
|
-
};
|
|
3367
|
-
return {
|
|
3368
|
-
type: "extension_ui_response",
|
|
3369
|
-
id: nativeId,
|
|
3370
|
-
value: pending.freeformSentinel,
|
|
3371
|
-
};
|
|
3372
|
-
}
|
|
3373
|
-
if (request.method === "select") {
|
|
3374
|
-
const mapped =
|
|
3375
|
-
pending.optionValues.get(publicValue) ?? pending.displayValues.get(publicValue);
|
|
3376
|
-
if (mapped === undefined) {
|
|
3377
|
-
throw new OmpPublicError("OMP selection response is invalid");
|
|
3378
|
-
}
|
|
3379
|
-
return { type: "extension_ui_response", id: nativeId, value: mapped };
|
|
3380
|
-
}
|
|
3381
|
-
return { type: "extension_ui_response", id: nativeId, value: publicValue };
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
private resolvePermissionByNativeId(nativeId: string): void {
|
|
3385
|
-
for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
|
|
3386
|
-
for (const [permissionId, pending] of permissions) {
|
|
3387
|
-
if (pending.nativeId !== nativeId) continue;
|
|
3388
|
-
permissions.delete(permissionId);
|
|
3389
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
3390
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3391
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
private resolveTurnPermissions(turnId: string): void {
|
|
3398
|
-
if (this.pendingFreeformSelection?.turnId === turnId) this.pendingFreeformSelection = null;
|
|
3399
|
-
this.resolvePermissions((pending) => pending.turnId === turnId, true);
|
|
3400
|
-
this.resolveToolPermissions((pending) => pending.turnId === turnId, true);
|
|
3401
|
-
}
|
|
3402
|
-
private resolveAllPermissions(cancelNative = false): void {
|
|
3403
|
-
this.pendingFreeformSelection = null;
|
|
3404
|
-
this.resolvePermissions(() => true, cancelNative);
|
|
3405
|
-
this.resolveToolPermissions(() => true, cancelNative);
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
private resolvePermissions(
|
|
3409
|
-
matches: (pending: PendingPermission) => boolean,
|
|
3410
|
-
cancelNative: boolean,
|
|
3411
|
-
): void {
|
|
3412
|
-
const permissionIds = new Set<string>();
|
|
3413
|
-
for (const permissions of [this.pendingPermissions, this.inFlightPermissions]) {
|
|
3414
|
-
for (const [permissionId, pending] of permissions) {
|
|
3415
|
-
if (!matches(pending)) continue;
|
|
3416
|
-
permissions.delete(permissionId);
|
|
3417
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
3418
|
-
permissionIds.add(permissionId);
|
|
3419
|
-
if (cancelNative && permissions === this.pendingPermissions) {
|
|
3420
|
-
void pending.runtime
|
|
3421
|
-
.respondToExtensionUi({
|
|
3422
|
-
type: "extension_ui_response",
|
|
3423
|
-
id: pending.nativeId,
|
|
3424
|
-
cancelled: true,
|
|
3425
|
-
})
|
|
3426
|
-
.catch(() => {
|
|
3427
|
-
if (!this.closed && !this.runtimeDead) {
|
|
3428
|
-
this.invalidateRuntime("OMP permission cancellation failed");
|
|
3429
|
-
}
|
|
3430
|
-
});
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
for (const permissionId of permissionIds) {
|
|
3435
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3436
|
-
}
|
|
3437
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3438
|
-
}
|
|
3439
|
-
private resolveToolPermissions(
|
|
3440
|
-
matches: (pending: PendingToolPermission) => boolean,
|
|
3441
|
-
cancelNative: boolean,
|
|
3442
|
-
): void {
|
|
3443
|
-
const permissionIds = new Set<string>();
|
|
3444
|
-
for (const permissions of [this.pendingToolPermissions, this.inFlightToolPermissions]) {
|
|
3445
|
-
for (const [permissionId, pending] of permissions) {
|
|
3446
|
-
if (!matches(pending)) continue;
|
|
3447
|
-
permissions.delete(permissionId);
|
|
3448
|
-
if (pending.timer !== undefined) this.scheduler.clear(pending.timer);
|
|
3449
|
-
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
3450
|
-
permissionIds.add(permissionId);
|
|
3451
|
-
if (cancelNative && permissions === this.pendingToolPermissions) {
|
|
3452
|
-
void pending.runtime
|
|
3453
|
-
.respondToToolApproval({
|
|
3454
|
-
type: "tool_approval_response",
|
|
3455
|
-
id: pending.nativeId,
|
|
3456
|
-
toolCallId: pending.toolCallId,
|
|
3457
|
-
cancelled: true,
|
|
3458
|
-
})
|
|
3459
|
-
.catch(() => {
|
|
3460
|
-
if (!this.closed && !this.runtimeDead) {
|
|
3461
|
-
this.invalidateRuntime("OMP tool permission cancellation failed");
|
|
3462
|
-
}
|
|
3463
|
-
});
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
}
|
|
3467
|
-
for (const permissionId of permissionIds) {
|
|
3468
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3469
|
-
}
|
|
3470
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
private armToolPermissionTimeout(permissionId: string, pending: PendingToolPermission): void {
|
|
3474
|
-
if (pending.expiresAt === undefined) return;
|
|
3475
|
-
const remainingMs = Math.max(0, pending.expiresAt - Date.now());
|
|
3476
|
-
pending.timer = this.scheduler.set(() => {
|
|
3477
|
-
if (this.pendingToolPermissions.get(permissionId) !== pending) return;
|
|
3478
|
-
this.pendingToolPermissions.delete(permissionId);
|
|
3479
|
-
this.inFlightToolPermissions.set(permissionId, pending);
|
|
3480
|
-
void pending.runtime
|
|
3481
|
-
.respondToToolApproval({
|
|
3482
|
-
type: "tool_approval_response",
|
|
3483
|
-
id: pending.nativeId,
|
|
3484
|
-
toolCallId: pending.toolCallId,
|
|
3485
|
-
cancelled: true,
|
|
3486
|
-
timedOut: true,
|
|
3487
|
-
})
|
|
3488
|
-
.then(
|
|
3489
|
-
() => {
|
|
3490
|
-
if (this.inFlightToolPermissions.get(permissionId) !== pending) return;
|
|
3491
|
-
this.inFlightToolPermissions.delete(permissionId);
|
|
3492
|
-
this.resolvedToolApprovalIds.add(pending.nativeId);
|
|
3493
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3494
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3495
|
-
},
|
|
3496
|
-
() => this.handleRuntimeFailure(),
|
|
3497
|
-
);
|
|
3498
|
-
}, remainingMs);
|
|
3499
|
-
}
|
|
3500
|
-
|
|
3501
|
-
private armPermissionTimeout(permissionId: string, pending: PendingPermission): void {
|
|
3502
|
-
if (pending.expiresAt === undefined) return;
|
|
3503
|
-
const remainingMs = Math.max(0, pending.expiresAt - Date.now());
|
|
3504
|
-
pending.timer = this.scheduler.set(() => {
|
|
3505
|
-
if (this.pendingPermissions.get(permissionId) !== pending) return;
|
|
3506
|
-
this.pendingPermissions.delete(permissionId);
|
|
3507
|
-
if (!this.permissionOwnerIsCurrent(pending)) {
|
|
3508
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3509
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3510
|
-
return;
|
|
3511
|
-
}
|
|
3512
|
-
this.inFlightPermissions.set(permissionId, pending);
|
|
3513
|
-
void pending.runtime
|
|
3514
|
-
.respondToExtensionUi({
|
|
3515
|
-
type: "extension_ui_response",
|
|
3516
|
-
id: pending.nativeId,
|
|
3517
|
-
cancelled: true,
|
|
3518
|
-
timedOut: true,
|
|
3519
|
-
})
|
|
3520
|
-
.then(
|
|
3521
|
-
() => {
|
|
3522
|
-
if (this.inFlightPermissions.get(permissionId) !== pending) return;
|
|
3523
|
-
this.inFlightPermissions.delete(permissionId);
|
|
3524
|
-
this.emit({ type: "session.permission_resolved", sessionId: this.id, permissionId });
|
|
3525
|
-
this.reevaluateDeferredPermissionTerminal();
|
|
3526
|
-
},
|
|
3527
|
-
() => this.handleRuntimeFailure(),
|
|
3528
|
-
);
|
|
3529
|
-
}, remainingMs);
|
|
3530
|
-
}
|
|
3531
|
-
|
|
3532
|
-
private permissionOwnerIsCurrent(pending: PendingPermission | PendingToolPermission): boolean {
|
|
3533
|
-
if (
|
|
3534
|
-
this.closed ||
|
|
3535
|
-
this.runtimeDead ||
|
|
3536
|
-
pending.generation !== this.generation ||
|
|
3537
|
-
pending.runtime !== this.runtime
|
|
3538
|
-
) {
|
|
3539
|
-
return false;
|
|
3540
|
-
}
|
|
3541
|
-
if (pending.turnId === undefined) return true;
|
|
3542
|
-
return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
|
|
3543
|
-
}
|
|
3544
2706
|
|
|
3545
2707
|
private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
|
|
3546
|
-
const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
|
|
3547
|
-
pending.turnId === turn.turnId;
|
|
3548
2708
|
return (
|
|
3549
2709
|
turn.awaitingPermissionEvidence ||
|
|
3550
|
-
|
|
3551
|
-
[...this.inFlightPermissions.values()].some(ownsTurn) ||
|
|
3552
|
-
[...this.pendingToolPermissions.values()].some(ownsTurn) ||
|
|
3553
|
-
[...this.inFlightToolPermissions.values()].some(ownsTurn) ||
|
|
2710
|
+
this.permissions.hasForTurn(turn.turnId) ||
|
|
3554
2711
|
turn.activeToolCallIds.size > 0 ||
|
|
3555
2712
|
turn.steersInFlight > 0 ||
|
|
3556
2713
|
(includeChildren && Boolean(this.subsessions?.hasActiveChildren()))
|
|
@@ -3994,7 +3151,7 @@ export class OmpProviderSession {
|
|
|
3994
3151
|
turn.terminal = true;
|
|
3995
3152
|
this.usage.stopPoll(turn);
|
|
3996
3153
|
this.cancelAmbiguousTerminal(turn);
|
|
3997
|
-
this.resolveTurnPermissions(turn.turnId);
|
|
3154
|
+
this.permissions.resolveTurnPermissions(turn.turnId);
|
|
3998
3155
|
this.projector.finishTurn(turn.turnId);
|
|
3999
3156
|
if (this.activeTurn === turn) this.activeTurn = null;
|
|
4000
3157
|
}
|
|
@@ -4105,7 +3262,7 @@ export class OmpProviderSession {
|
|
|
4105
3262
|
}
|
|
4106
3263
|
}
|
|
4107
3264
|
this.publishPendingUsers(turn);
|
|
4108
|
-
this.resolveTurnPermissions(turn.turnId);
|
|
3265
|
+
this.permissions.resolveTurnPermissions(turn.turnId);
|
|
4109
3266
|
try {
|
|
4110
3267
|
this.projector.finishTurn(turn.turnId, preserveCompactions);
|
|
4111
3268
|
} catch (error) {
|
|
@@ -4137,7 +3294,7 @@ export class OmpProviderSession {
|
|
|
4137
3294
|
this.imageMaterializer.clear();
|
|
4138
3295
|
if (this.closed || this.runtimeDead) return;
|
|
4139
3296
|
this.compaction.resetDiscardedEnds();
|
|
4140
|
-
this.resolveAllPermissions();
|
|
3297
|
+
this.permissions.resolveAllPermissions();
|
|
4141
3298
|
this.recoveryUsesNativeConfig ||=
|
|
4142
3299
|
this.configRefreshInFlight !== null || this.configRefreshDirty || this.configMutationInFlight;
|
|
4143
3300
|
const turn = this.activeTurn;
|