@modelprofile.com/flexharness 10.23.0 → 10.24.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts_compaction/plugins.d.ts +2 -2
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +9 -0
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +13 -1
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +2 -2
- package/dist_ts_stores_nosqldb/classes.recordstore.js +3 -3
- package/package.json +3 -3
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/readme.md +1 -1
- package/ts/utils.flexharness.ts +121 -0
- package/ts_compaction/plugins.ts +2 -2
- package/ts_compaction/smartagent.compaction.ts +5 -4
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +13 -0
- package/ts_stores_nosqldb/classes.recordstore.ts +5 -5
- package/ts_stores_nosqldb/readme.md +7 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessAbortError,
|
|
4
|
+
FlexHarnessNotFoundError,
|
|
5
|
+
FlexHarnessPermissionRejectedError,
|
|
6
|
+
FlexHarnessPermissionStateError,
|
|
7
|
+
FlexHarnessStoreConflictError,
|
|
8
|
+
FlexHarnessStoreCommitUncertainError,
|
|
9
|
+
FlexHarnessValidationError,
|
|
10
|
+
} from './errors.js';
|
|
11
|
+
import type {
|
|
12
|
+
IFlexAgentContextInvocation,
|
|
13
|
+
IFlexPermissionRequest,
|
|
14
|
+
IFlexPermissionRequestInput,
|
|
15
|
+
IFlexPermissionSnapshot,
|
|
16
|
+
IFlexSessionPermissionPolicyContext,
|
|
17
|
+
TFlexPermissionDecision,
|
|
18
|
+
TJsonValue,
|
|
19
|
+
} from './interfaces.js';
|
|
20
|
+
import { cloneSerializable, normalizeJsonValue } from './utils.json.js';
|
|
21
|
+
import { maxTransferMetadataBytes, publicSnapshot, truncateUtf8 } from './utils.transfer.js';
|
|
22
|
+
import { validateIdentifier } from './utils.validation.js';
|
|
23
|
+
import type {
|
|
24
|
+
IStoredSessionState,
|
|
25
|
+
IStorageState,
|
|
26
|
+
IActiveRun,
|
|
27
|
+
IPendingPermission,
|
|
28
|
+
} from './interfaces.flexharness.js';
|
|
29
|
+
import { requireSessionGeneration, combineErrors } from './utils.flexharness.js';
|
|
30
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
31
|
+
|
|
32
|
+
/** Permission requests and responses, and the permission policy of sessions. */
|
|
33
|
+
export class FlexHarnessPermissions<TScope> {
|
|
34
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
35
|
+
|
|
36
|
+
public async listPendingPermissions(
|
|
37
|
+
scopeId: string,
|
|
38
|
+
sessionId?: string,
|
|
39
|
+
): Promise<IFlexPermissionRequest[]> {
|
|
40
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
41
|
+
await state.scopeQueue;
|
|
42
|
+
this.core.assertStateAcceptingWork(state);
|
|
43
|
+
return publicSnapshot([...state.pendingPermissions.values()]
|
|
44
|
+
.map((pending) => pending.request)
|
|
45
|
+
.filter((request) => !sessionId || request.sessionId === sessionId)
|
|
46
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async respondToPermission(
|
|
50
|
+
scopeId: string,
|
|
51
|
+
sessionId: string,
|
|
52
|
+
permissionId: string,
|
|
53
|
+
decision: TFlexPermissionDecision,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
validateIdentifier(permissionId, 'permissionId');
|
|
56
|
+
const { scope, state } = await this.core.resolveState(scopeId);
|
|
57
|
+
const pending = state.pendingPermissions.get(permissionId);
|
|
58
|
+
if (!pending || pending.request.sessionId !== sessionId) {
|
|
59
|
+
throw new FlexHarnessNotFoundError('Permission', permissionId);
|
|
60
|
+
}
|
|
61
|
+
const context = this.core.compaction.createCompactorContext(scopeId, scope.scope, state.storageKey, sessionId);
|
|
62
|
+
const response = pending.responseQueue.then(() => this.core.compaction.withCompactorContext(
|
|
63
|
+
context,
|
|
64
|
+
() => this.applyPermissionResponse(state, pending, decision, context),
|
|
65
|
+
));
|
|
66
|
+
pending.responseQueue = response.then(() => undefined, () => undefined);
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async requestPermission(
|
|
71
|
+
state: IStorageState,
|
|
72
|
+
run: IActiveRun,
|
|
73
|
+
input: IFlexPermissionRequestInput,
|
|
74
|
+
exactMetadata?: TJsonValue,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
validateIdentifier(input.kind, 'permission kind');
|
|
77
|
+
validateIdentifier(input.description, 'permission description');
|
|
78
|
+
if (input.rememberKey !== undefined) validateIdentifier(input.rememberKey, 'rememberKey');
|
|
79
|
+
if (input.toolCallId !== undefined) validateIdentifier(input.toolCallId, 'toolCallId');
|
|
80
|
+
run.controller.signal.throwIfAborted();
|
|
81
|
+
await run.stored.permissionQueue;
|
|
82
|
+
run.controller.signal.throwIfAborted();
|
|
83
|
+
if (state.activeRuns.get(run.sessionId) !== run || run.callbacksClosed) {
|
|
84
|
+
throw new FlexHarnessPermissionStateError(`Run "${run.runId}" no longer accepts permission requests.`);
|
|
85
|
+
}
|
|
86
|
+
if (!run.permissionPolicy && input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const candidate: IFlexPermissionRequest = {
|
|
90
|
+
permissionId: plugins.crypto.randomUUID(),
|
|
91
|
+
scopeId: run.scopeId,
|
|
92
|
+
sessionId: run.sessionId,
|
|
93
|
+
...requireSessionGeneration(run.stored.session),
|
|
94
|
+
runId: run.runId,
|
|
95
|
+
kind: input.kind,
|
|
96
|
+
description: truncateUtf8(input.description, maxTransferMetadataBytes),
|
|
97
|
+
...(input.toolCallId ? { toolCallId: input.toolCallId } : {}),
|
|
98
|
+
...(input.rememberKey ? { rememberKey: input.rememberKey } : {}),
|
|
99
|
+
...(exactMetadata !== undefined
|
|
100
|
+
? { metadata: cloneSerializable(exactMetadata) }
|
|
101
|
+
: input.metadata === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { metadata: normalizeJsonValue(input.metadata, this.core.toolOutputLimits) }),
|
|
104
|
+
createdAt: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
const policyDecision = run.permissionPolicy
|
|
107
|
+
? await this.decideSessionPermissionPolicy(state, run, candidate)
|
|
108
|
+
: 'inherit';
|
|
109
|
+
if (policyDecision === 'allow' || policyDecision === 'deny') {
|
|
110
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
111
|
+
type: 'permission.resolved',
|
|
112
|
+
runId: run.runId,
|
|
113
|
+
request: publicSnapshot(candidate),
|
|
114
|
+
decision: policyDecision === 'allow' ? 'once' : 'reject',
|
|
115
|
+
origin: 'policy',
|
|
116
|
+
});
|
|
117
|
+
if (policyDecision === 'deny') {
|
|
118
|
+
const rejection = this.core.trustInternalError(
|
|
119
|
+
new FlexHarnessPermissionRejectedError(candidate.permissionId),
|
|
120
|
+
);
|
|
121
|
+
this.core.runs.abortRunInternally(run, rejection);
|
|
122
|
+
throw rejection;
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (policyDecision === 'inherit'
|
|
127
|
+
&& input.rememberKey && run.stored.rememberedPermissionKeys.has(input.rememberKey)) return;
|
|
128
|
+
const request: IFlexPermissionRequest = { ...candidate };
|
|
129
|
+
if (policyDecision === 'ask') {
|
|
130
|
+
delete request.rememberKey;
|
|
131
|
+
request.requiresExplicitResponse = true;
|
|
132
|
+
}
|
|
133
|
+
let resolvePermission!: () => void;
|
|
134
|
+
let rejectPermission!: (error: unknown) => void;
|
|
135
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
136
|
+
resolvePermission = resolve;
|
|
137
|
+
rejectPermission = reject;
|
|
138
|
+
});
|
|
139
|
+
void promise.catch(() => undefined);
|
|
140
|
+
const abortListener = () => {
|
|
141
|
+
const pending = state.pendingPermissions.get(request.permissionId);
|
|
142
|
+
if (pending) this.rejectPending(state, pending, run.controller.signal.reason);
|
|
143
|
+
};
|
|
144
|
+
const pending: IPendingPermission = {
|
|
145
|
+
request,
|
|
146
|
+
run,
|
|
147
|
+
resolve: resolvePermission,
|
|
148
|
+
reject: rejectPermission,
|
|
149
|
+
settled: false,
|
|
150
|
+
responding: false,
|
|
151
|
+
responseQueue: Promise.resolve(),
|
|
152
|
+
abortListener,
|
|
153
|
+
};
|
|
154
|
+
const closePublication = this.core.openPublication(state, run.sessionId);
|
|
155
|
+
try {
|
|
156
|
+
state.pendingPermissions.set(request.permissionId, pending);
|
|
157
|
+
run.pendingPermissionIds.add(request.permissionId);
|
|
158
|
+
run.controller.signal.addEventListener('abort', abortListener, { once: true });
|
|
159
|
+
try {
|
|
160
|
+
await this.core.mutateScope(state, () => {
|
|
161
|
+
if (state.sessions.get(run.sessionId) !== run.stored) {
|
|
162
|
+
throw new FlexHarnessAbortError('The session was deleted while permission was pending.');
|
|
163
|
+
}
|
|
164
|
+
run.stored.session.status = 'waiting_permission';
|
|
165
|
+
run.stored.session.activity.status = 'waiting_permission';
|
|
166
|
+
run.stored.session.updatedAt = new Date().toISOString();
|
|
167
|
+
}, true);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.forceRejectPending(state, pending, this.core.projectExternalError(run, error, 'persistence'));
|
|
170
|
+
return promise;
|
|
171
|
+
}
|
|
172
|
+
if (!pending.settled) {
|
|
173
|
+
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
174
|
+
type: 'permission.requested',
|
|
175
|
+
runId: run.runId,
|
|
176
|
+
request: publicSnapshot(request),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
closePublication();
|
|
181
|
+
}
|
|
182
|
+
return promise;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async decideSessionPermissionPolicy(
|
|
186
|
+
state: IStorageState,
|
|
187
|
+
run: IActiveRun,
|
|
188
|
+
request: IFlexPermissionRequest,
|
|
189
|
+
): Promise<'inherit' | 'allow' | 'ask' | 'deny'> {
|
|
190
|
+
const policy = run.permissionPolicy!;
|
|
191
|
+
const signal = run.controller.signal;
|
|
192
|
+
const context: Readonly<IFlexSessionPermissionPolicyContext<unknown>> = Object.freeze({
|
|
193
|
+
scopeId: run.scopeId,
|
|
194
|
+
scope: run.scope,
|
|
195
|
+
storageKey: state.storageKey,
|
|
196
|
+
sessionId: run.sessionId,
|
|
197
|
+
...requireSessionGeneration(run.stored.session),
|
|
198
|
+
runId: run.runId,
|
|
199
|
+
configurationRef: run.stored.session.configurationRef!,
|
|
200
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
201
|
+
request: publicSnapshot(request),
|
|
202
|
+
signal,
|
|
203
|
+
});
|
|
204
|
+
let onAbort!: () => void;
|
|
205
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
206
|
+
onAbort = () => reject(signal.reason ?? this.core.trustInternalError(new FlexHarnessAbortError()));
|
|
207
|
+
});
|
|
208
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
209
|
+
if (signal.aborted) onAbort();
|
|
210
|
+
let decision: unknown;
|
|
211
|
+
try {
|
|
212
|
+
decision = await Promise.race([
|
|
213
|
+
Promise.resolve().then(() => {
|
|
214
|
+
signal.throwIfAborted();
|
|
215
|
+
return policy(context);
|
|
216
|
+
}),
|
|
217
|
+
aborted,
|
|
218
|
+
]);
|
|
219
|
+
if (!['inherit', 'allow', 'ask', 'deny'].includes(decision as string)) {
|
|
220
|
+
throw new FlexHarnessValidationError('Session permissionPolicy returned an invalid decision.');
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (signal.aborted) throw signal.reason;
|
|
224
|
+
const projected = this.core.projectExternalError(run, error, 'permissionPolicy');
|
|
225
|
+
this.core.runs.abortRunInternally(run, projected);
|
|
226
|
+
throw projected;
|
|
227
|
+
} finally {
|
|
228
|
+
signal.removeEventListener('abort', onAbort);
|
|
229
|
+
}
|
|
230
|
+
signal.throwIfAborted();
|
|
231
|
+
const generation = requireSessionGeneration(run.stored.session);
|
|
232
|
+
if (
|
|
233
|
+
state.lifecycle !== 'active'
|
|
234
|
+
|| state.activeRuns.get(run.sessionId) !== run
|
|
235
|
+
|| state.sessions.get(run.sessionId) !== run.stored
|
|
236
|
+
|| run.callbacksClosed
|
|
237
|
+
|| generation.sessionGenerationId !== request.sessionGenerationId
|
|
238
|
+
|| generation.sessionGenerationSequence !== request.sessionGenerationSequence
|
|
239
|
+
) {
|
|
240
|
+
const ownershipError = this.core.trustInternalError(new FlexHarnessAbortError(
|
|
241
|
+
'Permission policy lost its exact run ownership.',
|
|
242
|
+
));
|
|
243
|
+
this.core.runs.abortRunInternally(run, ownershipError);
|
|
244
|
+
throw ownershipError;
|
|
245
|
+
}
|
|
246
|
+
return decision as 'inherit' | 'allow' | 'ask' | 'deny';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async applyPermissionResponse(
|
|
250
|
+
state: IStorageState,
|
|
251
|
+
pending: IPendingPermission,
|
|
252
|
+
decision: TFlexPermissionDecision,
|
|
253
|
+
context: IFlexAgentContextInvocation<TScope>,
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
if (pending.settled) {
|
|
256
|
+
throw new FlexHarnessPermissionStateError(`Permission "${pending.request.permissionId}" has already been resolved.`);
|
|
257
|
+
}
|
|
258
|
+
if (!['once', 'always', 'reject'].includes(decision)) {
|
|
259
|
+
throw new FlexHarnessPermissionStateError(`Unknown permission decision "${decision}".`);
|
|
260
|
+
}
|
|
261
|
+
if (decision === 'always' && !pending.request.rememberKey) {
|
|
262
|
+
throw new FlexHarnessPermissionStateError('The always decision requires rememberKey.');
|
|
263
|
+
}
|
|
264
|
+
const stored = pending.run.stored;
|
|
265
|
+
const rememberKey = pending.request.rememberKey;
|
|
266
|
+
let addedRememberKey = false;
|
|
267
|
+
pending.responding = true;
|
|
268
|
+
try {
|
|
269
|
+
if (decision === 'always' && rememberKey) {
|
|
270
|
+
await this.mutatePermissions(state, stored, () => {
|
|
271
|
+
addedRememberKey = !stored.rememberedPermissionKeys.has(rememberKey);
|
|
272
|
+
stored.rememberedPermissionKeys.add(rememberKey);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
await this.core.mutateScope(state, () => {
|
|
276
|
+
if (state.sessions.get(pending.request.sessionId) !== stored) {
|
|
277
|
+
throw new FlexHarnessAbortError('The session was deleted while permission was resolving.');
|
|
278
|
+
}
|
|
279
|
+
const hasOtherPending = [...state.pendingPermissions.values()].some((entry) =>
|
|
280
|
+
entry !== pending && !entry.settled && entry.request.sessionId === pending.request.sessionId);
|
|
281
|
+
stored.session.status = hasOtherPending ? 'waiting_permission' : 'running';
|
|
282
|
+
stored.session.activity.status = hasOtherPending ? 'waiting_permission' : 'running';
|
|
283
|
+
stored.session.updatedAt = new Date().toISOString();
|
|
284
|
+
}, true);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
pending.responding = false;
|
|
287
|
+
const projected = this.core.projectExternalError(pending.run, error, 'persistence');
|
|
288
|
+
const rollbackErrors = await this.rollbackRememberedGrant(
|
|
289
|
+
state,
|
|
290
|
+
pending,
|
|
291
|
+
addedRememberKey,
|
|
292
|
+
rememberKey,
|
|
293
|
+
);
|
|
294
|
+
if (
|
|
295
|
+
pending.abortReason === undefined
|
|
296
|
+
&& state.sessions.get(pending.request.sessionId) === stored
|
|
297
|
+
&& stored.session.status !== 'waiting_permission'
|
|
298
|
+
) {
|
|
299
|
+
try {
|
|
300
|
+
await this.core.mutateScope(state, () => {
|
|
301
|
+
stored.session.status = 'waiting_permission';
|
|
302
|
+
stored.session.activity.status = 'waiting_permission';
|
|
303
|
+
stored.session.updatedAt = new Date().toISOString();
|
|
304
|
+
}, true);
|
|
305
|
+
} catch (rollbackError) {
|
|
306
|
+
rollbackErrors.push(this.core.projectExternalError(pending.run, rollbackError, 'persistence'));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (pending.abortReason !== undefined) {
|
|
310
|
+
this.forceRejectPending(state, pending, pending.abortReason);
|
|
311
|
+
}
|
|
312
|
+
if (rollbackErrors.length > 0 || state.lifecycle === 'fenced') {
|
|
313
|
+
if (!pending.settled) this.forceRejectPending(state, pending, projected);
|
|
314
|
+
const combined = combineErrors([
|
|
315
|
+
...(pending.abortReason === undefined ? [] : [pending.abortReason]),
|
|
316
|
+
projected,
|
|
317
|
+
...rollbackErrors,
|
|
318
|
+
]);
|
|
319
|
+
await this.core.lifecycle.fenceNamespace(state, pending.run, combined);
|
|
320
|
+
throw combined;
|
|
321
|
+
}
|
|
322
|
+
if (pending.abortReason !== undefined) throw pending.abortReason;
|
|
323
|
+
throw projected;
|
|
324
|
+
}
|
|
325
|
+
if (pending.abortReason !== undefined) {
|
|
326
|
+
const rollbackErrors = await this.rollbackRememberedGrant(state, pending, addedRememberKey, rememberKey);
|
|
327
|
+
pending.responding = false;
|
|
328
|
+
this.forceRejectPending(state, pending, pending.abortReason);
|
|
329
|
+
if (rollbackErrors.length > 0) {
|
|
330
|
+
const combined = combineErrors([pending.abortReason, ...rollbackErrors]);
|
|
331
|
+
await this.core.lifecycle.fenceNamespace(state, pending.run, combined);
|
|
332
|
+
throw combined;
|
|
333
|
+
}
|
|
334
|
+
throw pending.abortReason;
|
|
335
|
+
}
|
|
336
|
+
pending.settled = true;
|
|
337
|
+
pending.responding = false;
|
|
338
|
+
state.pendingPermissions.delete(pending.request.permissionId);
|
|
339
|
+
pending.run.pendingPermissionIds.delete(pending.request.permissionId);
|
|
340
|
+
pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
|
|
341
|
+
this.core.emitEvent(pending.request.scopeId, pending.run.stored.session, {
|
|
342
|
+
type: 'permission.resolved',
|
|
343
|
+
runId: pending.request.runId,
|
|
344
|
+
request: publicSnapshot(pending.request),
|
|
345
|
+
decision,
|
|
346
|
+
});
|
|
347
|
+
if (decision === 'reject') {
|
|
348
|
+
const rejection = this.core.trustInternalError(
|
|
349
|
+
new FlexHarnessPermissionRejectedError(pending.request.permissionId),
|
|
350
|
+
);
|
|
351
|
+
this.core.runs.abortRunInternally(pending.run, rejection, context);
|
|
352
|
+
pending.reject(rejection);
|
|
353
|
+
} else {
|
|
354
|
+
pending.resolve();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async rollbackRememberedGrant(
|
|
359
|
+
state: IStorageState,
|
|
360
|
+
pending: IPendingPermission,
|
|
361
|
+
added: boolean,
|
|
362
|
+
rememberKey?: string,
|
|
363
|
+
): Promise<unknown[]> {
|
|
364
|
+
if (
|
|
365
|
+
!added
|
|
366
|
+
|| !rememberKey
|
|
367
|
+
|| !pending.run.stored.rememberedPermissionKeys.has(rememberKey)
|
|
368
|
+
) return [];
|
|
369
|
+
try {
|
|
370
|
+
await this.mutatePermissions(state, pending.run.stored, () => {
|
|
371
|
+
pending.run.stored.rememberedPermissionKeys.delete(rememberKey);
|
|
372
|
+
}, true);
|
|
373
|
+
return [];
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const initialError = this.core.projectExternalError(pending.run, error, 'persistence');
|
|
376
|
+
try {
|
|
377
|
+
await this.reconcileRememberedPermissionRemoval(
|
|
378
|
+
pending.run.stored,
|
|
379
|
+
state.storageKey,
|
|
380
|
+
rememberKey,
|
|
381
|
+
);
|
|
382
|
+
return [];
|
|
383
|
+
} catch (recoveryError) {
|
|
384
|
+
return [initialError, this.core.projectExternalError(pending.run, recoveryError, 'persistence')];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private async reconcileRememberedPermissionRemoval(
|
|
390
|
+
stored: IStoredSessionState,
|
|
391
|
+
storageKey: string,
|
|
392
|
+
rememberKey: string,
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
const operation = stored.permissionQueue.then(async () => {
|
|
395
|
+
let lastError: unknown;
|
|
396
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
397
|
+
try {
|
|
398
|
+
const current = await this.core.stores.permissions.load(storageKey, stored.session.sessionId);
|
|
399
|
+
if (!current || !current.rememberedPermissionKeys.includes(rememberKey)) {
|
|
400
|
+
stored.rememberedPermissionKeys = new Set(current?.rememberedPermissionKeys ?? []);
|
|
401
|
+
stored.permissionRevision = current?.revision ?? 0;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const snapshot = {
|
|
405
|
+
...current,
|
|
406
|
+
revision: current.revision + 1,
|
|
407
|
+
rememberedPermissionKeys: current.rememberedPermissionKeys.filter(
|
|
408
|
+
(entry) => entry !== rememberKey,
|
|
409
|
+
),
|
|
410
|
+
};
|
|
411
|
+
await this.core.stores.permissions.save(
|
|
412
|
+
storageKey,
|
|
413
|
+
stored.session.sessionId,
|
|
414
|
+
snapshot,
|
|
415
|
+
current.revision,
|
|
416
|
+
);
|
|
417
|
+
stored.rememberedPermissionKeys = new Set(snapshot.rememberedPermissionKeys);
|
|
418
|
+
stored.permissionRevision = snapshot.revision;
|
|
419
|
+
return;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
lastError = error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
throw lastError ?? new Error('Remembered permission rollback could not be reconciled.');
|
|
425
|
+
});
|
|
426
|
+
stored.permissionQueue = operation.then(() => undefined, () => undefined);
|
|
427
|
+
await operation;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
public rejectRunPermissions(state: IStorageState, run: IActiveRun, error: unknown): void {
|
|
431
|
+
for (const permissionId of [...run.pendingPermissionIds]) {
|
|
432
|
+
const pending = state.pendingPermissions.get(permissionId);
|
|
433
|
+
if (pending) this.rejectPending(state, pending, error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
public rejectPending(state: IStorageState, pending: IPendingPermission, error: unknown): void {
|
|
438
|
+
if (pending.settled) return;
|
|
439
|
+
if (pending.responding) {
|
|
440
|
+
pending.abortReason ??= error;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
this.forceRejectPending(state, pending, error);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private forceRejectPending(state: IStorageState, pending: IPendingPermission, error: unknown): void {
|
|
447
|
+
if (pending.settled) return;
|
|
448
|
+
pending.settled = true;
|
|
449
|
+
state.pendingPermissions.delete(pending.request.permissionId);
|
|
450
|
+
pending.run.pendingPermissionIds.delete(pending.request.permissionId);
|
|
451
|
+
pending.run.controller.signal.removeEventListener('abort', pending.abortListener);
|
|
452
|
+
pending.reject(error);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private mutatePermissions<TValue>(
|
|
456
|
+
state: IStorageState,
|
|
457
|
+
stored: IStoredSessionState,
|
|
458
|
+
mutation: () => TValue,
|
|
459
|
+
_allowDuringDrain = false,
|
|
460
|
+
): Promise<TValue> {
|
|
461
|
+
const operation = stored.permissionQueue.then(async () => {
|
|
462
|
+
const beforeRevision = stored.permissionRevision;
|
|
463
|
+
const beforeKeys = new Set(stored.rememberedPermissionKeys);
|
|
464
|
+
const beforeSnapshot = {
|
|
465
|
+
schemaVersion: 1 as const,
|
|
466
|
+
revision: beforeRevision,
|
|
467
|
+
rememberedPermissionKeys: [...beforeKeys].sort(),
|
|
468
|
+
};
|
|
469
|
+
const restore = () => {
|
|
470
|
+
stored.permissionRevision = beforeRevision;
|
|
471
|
+
stored.rememberedPermissionKeys = beforeKeys;
|
|
472
|
+
};
|
|
473
|
+
let result: TValue;
|
|
474
|
+
try {
|
|
475
|
+
result = mutation();
|
|
476
|
+
} catch (error) {
|
|
477
|
+
restore();
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
const snapshot = {
|
|
481
|
+
schemaVersion: 1 as const,
|
|
482
|
+
revision: beforeRevision + 1,
|
|
483
|
+
rememberedPermissionKeys: [...stored.rememberedPermissionKeys].sort(),
|
|
484
|
+
};
|
|
485
|
+
try {
|
|
486
|
+
await this.core.stores.permissions.save(
|
|
487
|
+
state.storageKey,
|
|
488
|
+
stored.session.sessionId,
|
|
489
|
+
snapshot,
|
|
490
|
+
beforeRevision,
|
|
491
|
+
);
|
|
492
|
+
stored.permissionRevision = snapshot.revision;
|
|
493
|
+
return result;
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (error instanceof FlexHarnessStoreConflictError) {
|
|
496
|
+
restore();
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
if (error instanceof FlexHarnessStoreCommitUncertainError) {
|
|
500
|
+
stored.permissionRevision = snapshot.revision;
|
|
501
|
+
state.lifecycle = 'fenced';
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
let current: IFlexPermissionSnapshot | undefined;
|
|
505
|
+
let reconciliationError: unknown;
|
|
506
|
+
try {
|
|
507
|
+
current = await this.core.stores.permissions.load(
|
|
508
|
+
state.storageKey,
|
|
509
|
+
stored.session.sessionId,
|
|
510
|
+
);
|
|
511
|
+
} catch (loadError) {
|
|
512
|
+
reconciliationError = loadError;
|
|
513
|
+
}
|
|
514
|
+
if (current && JSON.stringify(current) === JSON.stringify(snapshot)) {
|
|
515
|
+
stored.permissionRevision = snapshot.revision;
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
if (
|
|
519
|
+
(current === undefined && beforeRevision === 0)
|
|
520
|
+
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
521
|
+
) {
|
|
522
|
+
restore();
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
stored.permissionRevision = snapshot.revision;
|
|
526
|
+
state.lifecycle = 'fenced';
|
|
527
|
+
throw reconciliationError === undefined
|
|
528
|
+
? error
|
|
529
|
+
: combineErrors([error, reconciliationError]);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
stored.permissionQueue = operation.then(() => undefined, () => undefined);
|
|
533
|
+
return operation;
|
|
534
|
+
}
|
|
535
|
+
}
|