@agentplat/inference-control 0.3.0-beta.1 → 0.3.0-beta.2

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/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- import { canonicalizeControlJsonV1, digestControlJsonV1, utf8ByteLength, } from './canonical.js';
1
+ import { canonicalizeControlJsonV1, digestControlJsonV1, utf8ByteLength, } from "./canonical.js";
2
2
  export const MAX_ACTION_INPUT_BYTES_V1 = 65_536;
3
3
  export const MAX_CONTROL_JSON_DEPTH_V1 = 64;
4
4
  export const MAX_CONTROL_JSON_NODES_V1 = 65_536;
@@ -6,18 +6,18 @@ export function canonicalControlJson(value) {
6
6
  return canonicalizeControlJsonV1(value);
7
7
  }
8
8
  export function controlDigest(domain, value) {
9
- if (domain === 'grant')
10
- return digestControlJsonV1('state', value);
9
+ if (domain === "grant")
10
+ return digestControlJsonV1("state", value);
11
11
  return digestControlJsonV1(domain, value);
12
12
  }
13
13
  export function actionInputDigest(input) {
14
- return controlDigest('action-input', input);
14
+ return controlDigest("action-input", input);
15
15
  }
16
16
  export function scopeDigest(scope) {
17
- return controlDigest('scope', scope);
17
+ return controlDigest("scope", scope);
18
18
  }
19
19
  export function actionDigest(grant, binding) {
20
- return controlDigest('action', {
20
+ return controlDigest("action", {
21
21
  scopeDigest: grant.scopeDigest,
22
22
  namespace: binding.namespace,
23
23
  toolId: binding.toolId,
@@ -42,10 +42,8 @@ function idempotencyKey(scope, key) {
42
42
  }
43
43
  function assertTime(time) {
44
44
  if (!Number.isSafeInteger(time) || time < 0)
45
- throw new TypeError('logicalTimeMs must be a non-negative safe integer');
45
+ throw new TypeError("logicalTimeMs must be a non-negative safe integer");
46
46
  }
47
- const grantReserveCapability = Symbol('grant-ledger-reserve-capability');
48
- const grantOutcomeCapability = Symbol('grant-ledger-outcome-capability');
49
47
  /** In-memory, single-process ledger. Reservation is synchronous and linearized. */
50
48
  export class LocalGrantLedger {
51
49
  gatewayId;
@@ -61,25 +59,25 @@ export class LocalGrantLedger {
61
59
  grant.expiresAtLogicalMs <= grant.issuedAtLogicalMs ||
62
60
  grant.expiresAtLogicalMs - grant.issuedAtLogicalMs > 120_000 ||
63
61
  !grant.singleUse ||
64
- grant.status !== 'issued' ||
62
+ grant.status !== "issued" ||
65
63
  grant.reservation !== null ||
66
64
  !isActionScopeV1(grant.scope) ||
67
65
  scopeDigest(grant.scope) !== grant.scopeDigest ||
68
66
  !/^sha256:[0-9a-f]{64}$/.test(grant.inputDigest) ||
69
67
  !/^sha256:[0-9a-f]{64}$/.test(grant.actionDigest))
70
- throw new TypeError('Invalid Action Grant');
68
+ throw new TypeError("Invalid Action Grant");
71
69
  const prior = this.grants.get(grant.grantId);
72
70
  if (prior) {
73
- if (controlDigest('grant', prior) !==
74
- controlDigest('grant', grant))
75
- throw new Error('state_conflict');
71
+ if (controlDigest("grant", prior) !==
72
+ controlDigest("grant", grant))
73
+ throw new Error("state_conflict");
76
74
  return prior;
77
75
  }
78
76
  const key = idempotencyKey(grant.scopeDigest, grant.idempotencyKey);
79
77
  const previous = this.idempotency.get(key);
80
78
  if (previous) {
81
79
  if (previous.actionDigest !== grant.actionDigest)
82
- throw new Error('grant_idempotency_conflict');
80
+ throw new Error("grant_idempotency_conflict");
83
81
  return this.grants.get(previous.grantId);
84
82
  }
85
83
  const retained = freezeGrant(grant);
@@ -94,43 +92,6 @@ export class LocalGrantLedger {
94
92
  }));
95
93
  return retained;
96
94
  }
97
- [grantReserveCapability](grantId, gatewayId, time, authority) {
98
- this.time(time);
99
- const grant = this.require(grantId);
100
- if (grant.status !== 'issued')
101
- throw new Error('grant_consumed');
102
- if (time >= grant.expiresAtLogicalMs)
103
- return this.expire(grant);
104
- assertCurrentAuthority(grant.scope, grant.scopeDigest, authority);
105
- const reservation = Object.freeze({
106
- schemaVersion: 1,
107
- reservationId: `${grantId}:reservation:${grant.stateGeneration + 1}`,
108
- dispatchAttemptId: `${grantId}:dispatch:${grant.stateGeneration + 1}`,
109
- reservedByGatewayId: gatewayId,
110
- reservedStateGeneration: grant.stateGeneration + 1,
111
- authorityGeneration: authorityGenerationFor(grant.scope, authority),
112
- fencingToken: fencingTokenFor(grant.scope, authority),
113
- reservedAtLogicalMs: time,
114
- });
115
- return this.replace({
116
- ...grant,
117
- stateGeneration: grant.stateGeneration + 1,
118
- status: 'reserved',
119
- reservation,
120
- });
121
- }
122
- [grantOutcomeCapability](grantId, reservationId, dispatchAttemptId, status) {
123
- const grant = this.require(grantId);
124
- if (grant.status !== 'reserved' ||
125
- grant.reservation?.reservationId !== reservationId ||
126
- grant.reservation.dispatchAttemptId !== dispatchAttemptId)
127
- throw new Error('state_conflict');
128
- return this.replace({
129
- ...grant,
130
- stateGeneration: grant.stateGeneration + 1,
131
- status,
132
- });
133
- }
134
95
  snapshot() {
135
96
  return Object.freeze({
136
97
  schemaVersion: 1,
@@ -146,27 +107,27 @@ export class LocalGrantLedger {
146
107
  !Number.isSafeInteger(snapshot.highWaterLogicalMs) ||
147
108
  snapshot.highWaterLogicalMs < 0 ||
148
109
  snapshot.highWaterLogicalMs < this.highWater)
149
- throw new TypeError('Invalid Action Grant snapshot');
110
+ throw new TypeError("Invalid Action Grant snapshot");
150
111
  const grants = new Map();
151
112
  const idempotency = new Map();
152
113
  for (const source of snapshot.grants) {
153
114
  assertGrantSnapshot(source, snapshot.highWaterLogicalMs);
154
- const grant = source.status === 'reserved'
115
+ const grant = source.status === "reserved"
155
116
  ? {
156
117
  ...source,
157
- status: 'indeterminate',
118
+ status: "indeterminate",
158
119
  stateGeneration: source.stateGeneration + 1,
159
120
  }
160
121
  : source;
161
122
  const frozen = freezeGrant(grant);
162
123
  if (grants.has(frozen.grantId))
163
- throw new Error('state_conflict');
124
+ throw new Error("state_conflict");
164
125
  const key = idempotencyKey(frozen.scopeDigest, frozen.idempotencyKey);
165
126
  const previous = idempotency.get(key);
166
127
  if (previous && previous.actionDigest !== frozen.actionDigest)
167
- throw new Error('grant_idempotency_conflict');
128
+ throw new Error("grant_idempotency_conflict");
168
129
  if (previous)
169
- throw new Error('state_conflict');
130
+ throw new Error("state_conflict");
170
131
  grants.set(frozen.grantId, frozen);
171
132
  idempotency.set(key, Object.freeze({
172
133
  schemaVersion: 1,
@@ -188,48 +149,227 @@ export class LocalGrantLedger {
188
149
  get(grantId) {
189
150
  return this.grants.get(grantId);
190
151
  }
191
- replace(grant) {
192
- const frozen = freezeGrant(grant);
193
- this.grants.set(frozen.grantId, frozen);
194
- this.idempotency.set(idempotencyKey(frozen.scopeDigest, frozen.idempotencyKey), Object.freeze({
195
- schemaVersion: 1,
196
- scopeDigest: frozen.scopeDigest,
197
- idempotencyKey: frozen.idempotencyKey,
198
- actionDigest: frozen.actionDigest,
199
- grantId: frozen.grantId,
200
- retainedOutcome: frozen.status,
201
- }));
202
- return frozen;
152
+ observeLogicalTime(logicalTimeMs) {
153
+ this.time(logicalTimeMs);
203
154
  }
204
- expire(grant) {
205
- return this.replace({
206
- ...grant,
207
- stateGeneration: grant.stateGeneration + 1,
208
- status: 'expired',
155
+ loadGrant(grantId) {
156
+ return this.grants.get(grantId);
157
+ }
158
+ loadIdempotency(scope, key) {
159
+ return this.idempotency.get(idempotencyKey(scope, key));
160
+ }
161
+ createGrant(input) {
162
+ assertRepositoryPair(input.grant, input.idempotency);
163
+ const prior = this.grants.get(input.grant.grantId);
164
+ if (prior) {
165
+ const same = grantStateDigest(prior) === grantStateDigest(input.grant);
166
+ return Object.freeze({
167
+ status: same ? "existing" : "conflict",
168
+ conflictKind: same ? null : "grant",
169
+ grant: prior,
170
+ });
171
+ }
172
+ const key = idempotencyKey(input.idempotency.scopeDigest, input.idempotency.idempotencyKey);
173
+ const previous = this.idempotency.get(key);
174
+ if (previous) {
175
+ const found = this.grants.get(previous.grantId) ?? null;
176
+ const same = previous.actionDigest === input.idempotency.actionDigest &&
177
+ found !== null;
178
+ return Object.freeze({
179
+ status: same ? "existing" : "conflict",
180
+ conflictKind: same ? null : "idempotency",
181
+ grant: found,
182
+ });
183
+ }
184
+ const grant = freezeGrant(input.grant);
185
+ const record = freezeIdempotency(input.idempotency);
186
+ this.grants.set(grant.grantId, grant);
187
+ this.idempotency.set(key, record);
188
+ return Object.freeze({
189
+ status: "created",
190
+ conflictKind: null,
191
+ grant,
209
192
  });
210
193
  }
211
- require(id) {
212
- const grant = this.grants.get(id);
213
- if (!grant)
214
- throw new Error('grant_missing');
215
- return grant;
194
+ compareAndSwapGrant(input) {
195
+ const current = this.grants.get(input.grantId);
196
+ if (!current ||
197
+ current.stateGeneration !== input.expectedStateGeneration ||
198
+ grantStateDigest(current) !== input.expectedGrantDigest)
199
+ return Object.freeze({ status: "conflict", grant: current ?? null });
200
+ if (input.nextGrant.grantId !== input.grantId ||
201
+ input.nextGrant.scopeDigest !== current.scopeDigest ||
202
+ input.nextGrant.idempotencyKey !== current.idempotencyKey)
203
+ throw new Error("state_conflict");
204
+ assertRepositoryPair(input.nextGrant, input.nextIdempotency);
205
+ const grant = freezeGrant(input.nextGrant);
206
+ this.grants.set(grant.grantId, grant);
207
+ this.idempotency.set(idempotencyKey(grant.scopeDigest, grant.idempotencyKey), freezeIdempotency(input.nextIdempotency));
208
+ return Object.freeze({ status: "updated", grant });
216
209
  }
217
210
  time(value) {
218
211
  assertTime(value);
219
212
  if (value < this.highWater)
220
- throw new Error('logical_time_rollback');
213
+ throw new Error("logical_time_rollback");
221
214
  this.highWater = value;
222
215
  }
223
216
  }
224
- /** Gateway-only mutation helpers; intentionally not exported from this module. */
225
- function reserveGrantForGateway(ledger, grantId, gatewayId, time, authority) {
226
- return ledger[grantReserveCapability](grantId, gatewayId, time, authority);
227
- }
228
- function settleGrantForGateway(ledger, grantId, reservationId, dispatchAttemptId, status) {
229
- return ledger[grantOutcomeCapability](grantId, reservationId, dispatchAttemptId, status);
217
+ /** Issues through the same exact-create port used by durable repositories. */
218
+ export async function issueActionGrantV1(repository, source) {
219
+ assertIssuableGrant(source);
220
+ await repository.observeLogicalTime(source.issuedAtLogicalMs);
221
+ const grant = freezeGrant(source);
222
+ const idempotency = idempotencyForGrant(grant);
223
+ const result = await repository.createGrant({ grant, idempotency });
224
+ if (result.status === "conflict")
225
+ throw new Error(result.conflictKind === "idempotency"
226
+ ? "grant_idempotency_conflict"
227
+ : "state_conflict");
228
+ if (!result.grant)
229
+ throw new Error("repository_unavailable");
230
+ assertGrantSnapshot(result.grant, Number.MAX_SAFE_INTEGER);
231
+ if (result.status === "created") {
232
+ if (grantStateDigest(result.grant) !== grantStateDigest(grant))
233
+ throw new Error("state_conflict");
234
+ }
235
+ else if (result.grant.scopeDigest !== grant.scopeDigest ||
236
+ result.grant.idempotencyKey !== grant.idempotencyKey ||
237
+ result.grant.actionDigest !== grant.actionDigest) {
238
+ throw new Error("state_conflict");
239
+ }
240
+ return result.grant;
241
+ }
242
+ /** Resolves an indeterminate grant only from an explicit authoritative proof. */
243
+ export async function reconcileActionGrantV1(repository, input) {
244
+ const grant = await repository.loadGrant(input.grantId);
245
+ if (!grant)
246
+ throw new Error("grant_missing");
247
+ assertGrantSnapshot(grant, Number.MAX_SAFE_INTEGER);
248
+ if ((grant.status === "dispatched" || grant.status === "failed") &&
249
+ grant.status === input.outcome &&
250
+ grant.reservation?.reservationId === input.reservationId &&
251
+ grant.reservation.dispatchAttemptId === input.dispatchAttemptId)
252
+ return grant;
253
+ if (grant.status !== "indeterminate" ||
254
+ grant.reservation?.reservationId !== input.reservationId ||
255
+ grant.reservation.dispatchAttemptId !== input.dispatchAttemptId)
256
+ throw new Error("state_conflict");
257
+ const next = freezeGrant({
258
+ ...grant,
259
+ stateGeneration: grant.stateGeneration + 1,
260
+ status: input.outcome,
261
+ });
262
+ if (!(await compareAndSwapGrantState(repository, grant, next)))
263
+ throw new Error("state_conflict");
264
+ return next;
265
+ }
266
+ /** Gateway-only semantic mutation helpers; intentionally not exported. */
267
+ async function reserveGrantForGateway(repository, grantId, gatewayId, time, authority) {
268
+ await repository.observeLogicalTime(time);
269
+ const grant = await repository.loadGrant(grantId);
270
+ if (!grant)
271
+ throw new Error("grant_missing");
272
+ assertGrantSnapshot(grant, Number.MAX_SAFE_INTEGER);
273
+ if (grant.status !== "issued")
274
+ throw new Error("grant_consumed");
275
+ if (time >= grant.expiresAtLogicalMs) {
276
+ const expired = freezeGrant({
277
+ ...grant,
278
+ stateGeneration: grant.stateGeneration + 1,
279
+ status: "expired",
280
+ });
281
+ const written = await compareAndSwapGrantState(repository, grant, expired);
282
+ if (!written)
283
+ throw new Error("state_conflict");
284
+ throw new Error("grant_expired");
285
+ }
286
+ assertCurrentAuthority(grant.scope, grant.scopeDigest, authority);
287
+ const reservation = Object.freeze({
288
+ schemaVersion: 1,
289
+ reservationId: `${grantId}:reservation:${grant.stateGeneration + 1}`,
290
+ dispatchAttemptId: `${grantId}:dispatch:${grant.stateGeneration + 1}`,
291
+ reservedByGatewayId: gatewayId,
292
+ reservedStateGeneration: grant.stateGeneration + 1,
293
+ authorityGeneration: authorityGenerationFor(grant.scope, authority),
294
+ fencingToken: fencingTokenFor(grant.scope, authority),
295
+ reservedAtLogicalMs: time,
296
+ });
297
+ const next = freezeGrant({
298
+ ...grant,
299
+ stateGeneration: grant.stateGeneration + 1,
300
+ status: "reserved",
301
+ reservation,
302
+ });
303
+ if (!(await compareAndSwapGrantState(repository, grant, next)))
304
+ throw new Error("grant_consumed");
305
+ return next;
306
+ }
307
+ async function settleGrantForGateway(repository, grantId, reservationId, dispatchAttemptId, status) {
308
+ const grant = await repository.loadGrant(grantId);
309
+ if (!grant)
310
+ throw new Error("grant_missing");
311
+ assertGrantSnapshot(grant, Number.MAX_SAFE_INTEGER);
312
+ if (grant.status === status &&
313
+ grant.reservation?.reservationId === reservationId &&
314
+ grant.reservation.dispatchAttemptId === dispatchAttemptId)
315
+ return grant;
316
+ if (grant.status !== "reserved" ||
317
+ grant.reservation?.reservationId !== reservationId ||
318
+ grant.reservation.dispatchAttemptId !== dispatchAttemptId)
319
+ throw new Error("state_conflict");
320
+ const next = freezeGrant({
321
+ ...grant,
322
+ stateGeneration: grant.stateGeneration + 1,
323
+ status,
324
+ });
325
+ if (!(await compareAndSwapGrantState(repository, grant, next))) {
326
+ const current = await repository.loadGrant(grantId);
327
+ if (current?.status === status &&
328
+ current.reservation?.reservationId === reservationId &&
329
+ current.reservation.dispatchAttemptId === dispatchAttemptId)
330
+ return current;
331
+ throw new Error("state_conflict");
332
+ }
333
+ return next;
334
+ }
335
+ async function compareAndSwapGrantState(repository, current, next) {
336
+ const result = await repository.compareAndSwapGrant({
337
+ grantId: current.grantId,
338
+ expectedStateGeneration: current.stateGeneration,
339
+ expectedGrantDigest: grantStateDigest(current),
340
+ nextGrant: next,
341
+ nextIdempotency: idempotencyForGrant(next),
342
+ });
343
+ return (result.status === "updated" &&
344
+ result.grant !== null &&
345
+ grantStateDigest(result.grant) === grantStateDigest(next));
230
346
  }
231
347
  function assertGrantSnapshot(grant, highWater) {
232
348
  if (!isPlainRecord(grant) ||
349
+ !hasExactKeys(grant, [
350
+ "schemaVersion",
351
+ "grantId",
352
+ "stateGeneration",
353
+ "scope",
354
+ "scopeDigest",
355
+ "namespace",
356
+ "toolId",
357
+ "operation",
358
+ "actionBindingId",
359
+ "actionBindingVersion",
360
+ "handlerDigest",
361
+ "inputDigest",
362
+ "actionDigest",
363
+ "assessmentRequestId",
364
+ "assessmentId",
365
+ "assessmentTargetDigest",
366
+ "idempotencyKey",
367
+ "issuedAtLogicalMs",
368
+ "expiresAtLogicalMs",
369
+ "singleUse",
370
+ "status",
371
+ "reservation",
372
+ ]) ||
233
373
  grant.schemaVersion !== 1 ||
234
374
  !nonEmptyString(grant.grantId) ||
235
375
  !positiveInteger(grant.stateGeneration) ||
@@ -248,18 +388,28 @@ function assertGrantSnapshot(grant, highWater) {
248
388
  !isGrantStatus(grant.status) ||
249
389
  !nonEmptyString(grant.idempotencyKey) ||
250
390
  !nonEmptyString(grant.scopeDigest))
251
- throw new TypeError('Invalid Action Grant snapshot');
252
- if (grant.status === 'issued' || grant.status === 'expired') {
391
+ throw new TypeError("Invalid Action Grant snapshot");
392
+ if (grant.status === "issued" || grant.status === "expired") {
253
393
  if (grant.reservation !== null)
254
- throw new TypeError('Invalid Action Grant snapshot');
394
+ throw new TypeError("Invalid Action Grant snapshot");
255
395
  }
256
396
  else if (!isActionReservation(grant.reservation, highWater, grant)) {
257
- throw new TypeError('Invalid Action Grant snapshot');
397
+ throw new TypeError("Invalid Action Grant snapshot");
258
398
  }
259
399
  }
260
400
  function isActionReservation(value, highWater, grant) {
261
401
  return Boolean(value &&
262
402
  isPlainRecord(value) &&
403
+ hasExactKeys(value, [
404
+ "schemaVersion",
405
+ "reservationId",
406
+ "dispatchAttemptId",
407
+ "reservedByGatewayId",
408
+ "reservedStateGeneration",
409
+ "authorityGeneration",
410
+ "fencingToken",
411
+ "reservedAtLogicalMs",
412
+ ]) &&
263
413
  value.schemaVersion === 1 &&
264
414
  nonEmptyString(value.reservationId) &&
265
415
  nonEmptyString(value.dispatchAttemptId) &&
@@ -274,16 +424,56 @@ function isActionReservation(value, highWater, grant) {
274
424
  value.authorityGeneration >= 0)) &&
275
425
  (value.fencingToken === null || nonEmptyString(value.fencingToken)));
276
426
  }
427
+ function assertIssuableGrant(grant) {
428
+ assertGrantSnapshot(grant, grant.issuedAtLogicalMs);
429
+ if (grant.status !== "issued" || grant.reservation !== null)
430
+ throw new TypeError("Invalid Action Grant");
431
+ }
432
+ function grantStateDigest(grant) {
433
+ return controlDigest("grant", grant);
434
+ }
435
+ function idempotencyForGrant(grant) {
436
+ return Object.freeze({
437
+ schemaVersion: 1,
438
+ scopeDigest: grant.scopeDigest,
439
+ idempotencyKey: grant.idempotencyKey,
440
+ actionDigest: grant.actionDigest,
441
+ grantId: grant.grantId,
442
+ retainedOutcome: grant.status,
443
+ });
444
+ }
445
+ function freezeIdempotency(value) {
446
+ return Object.freeze({ ...value });
447
+ }
448
+ function assertRepositoryPair(grant, record) {
449
+ assertGrantSnapshot(grant, Number.MAX_SAFE_INTEGER);
450
+ if (!isPlainRecord(record) ||
451
+ !hasExactKeys(record, [
452
+ "schemaVersion",
453
+ "scopeDigest",
454
+ "idempotencyKey",
455
+ "actionDigest",
456
+ "grantId",
457
+ "retainedOutcome",
458
+ ]) ||
459
+ record.schemaVersion !== 1 ||
460
+ record.scopeDigest !== grant.scopeDigest ||
461
+ record.idempotencyKey !== grant.idempotencyKey ||
462
+ record.actionDigest !== grant.actionDigest ||
463
+ record.grantId !== grant.grantId ||
464
+ record.retainedOutcome !== grant.status)
465
+ throw new TypeError("Invalid Action Grant repository record");
466
+ }
277
467
  function assertCurrentAuthority(scope, expectedScopeDigest, authority) {
278
- if (authority.status !== 'current' ||
468
+ if (authority.status !== "current" ||
279
469
  !isActionScopeV1(authority.scope) ||
280
470
  scopeDigest(authority.scope) !== expectedScopeDigest)
281
- throw new Error('grant_scope_mismatch');
282
- if (scope.kind === 'coordinated' &&
471
+ throw new Error("grant_scope_mismatch");
472
+ if (scope.kind === "coordinated" &&
283
473
  (authority.authorityGeneration !== scope.authorityGeneration ||
284
474
  authority.fencingToken !== scope.fencingToken))
285
- throw new Error('grant_fence_stale');
286
- if (scope.kind === 'standalone' &&
475
+ throw new Error("grant_fence_stale");
476
+ if (scope.kind === "standalone" &&
287
477
  ((authority.authorityGeneration != null) !==
288
478
  (authority.fencingToken != null) ||
289
479
  (authority.authorityGeneration != null &&
@@ -291,11 +481,11 @@ function assertCurrentAuthority(scope, expectedScopeDigest, authority) {
291
481
  authority.authorityGeneration < 0)) ||
292
482
  (authority.fencingToken != null &&
293
483
  !nonEmptyString(authority.fencingToken))))
294
- throw new Error('grant_fence_stale');
484
+ throw new Error("grant_fence_stale");
295
485
  }
296
486
  export function normalizeAuthorityResultV1(resolver, scope, actionDigestValue, value) {
297
487
  if (!isPlainRecord(value) || value.schemaVersion !== 1)
298
- throw new Error('authority_result_invalid');
488
+ throw new Error("authority_result_invalid");
299
489
  const expectedScopeDigest = scopeDigest(scope);
300
490
  const common = value.resolverId === resolver.resolverId &&
301
491
  value.resolverVersion === resolver.resolverVersion &&
@@ -304,55 +494,55 @@ export function normalizeAuthorityResultV1(resolver, scope, actionDigestValue, v
304
494
  isDigest(value.scopeDigest) &&
305
495
  isDigest(value.actionDigest);
306
496
  if (!common)
307
- throw new Error('authority_result_invalid');
308
- if (value.status === 'stale' || value.status === 'unavailable') {
497
+ throw new Error("authority_result_invalid");
498
+ if (value.status === "stale" || value.status === "unavailable") {
309
499
  if (!hasExactKeys(value, [
310
- 'schemaVersion',
311
- 'status',
312
- 'resolverId',
313
- 'resolverVersion',
314
- 'scopeDigest',
315
- 'actionDigest',
500
+ "schemaVersion",
501
+ "status",
502
+ "resolverId",
503
+ "resolverVersion",
504
+ "scopeDigest",
505
+ "actionDigest",
316
506
  ]))
317
- throw new Error('authority_result_invalid');
507
+ throw new Error("authority_result_invalid");
318
508
  return Object.freeze({ ...value });
319
509
  }
320
- if (value.status !== 'current' ||
510
+ if (value.status !== "current" ||
321
511
  !hasExactKeys(value, [
322
- 'schemaVersion',
323
- 'status',
324
- 'resolverId',
325
- 'resolverVersion',
326
- 'scopeDigest',
327
- 'actionDigest',
328
- 'scope',
329
- 'authorityGeneration',
330
- 'fencingToken',
512
+ "schemaVersion",
513
+ "status",
514
+ "resolverId",
515
+ "resolverVersion",
516
+ "scopeDigest",
517
+ "actionDigest",
518
+ "scope",
519
+ "authorityGeneration",
520
+ "fencingToken",
331
521
  ]) ||
332
522
  !isActionScopeV1(value.scope) ||
333
523
  scopeDigest(value.scope) !== expectedScopeDigest ||
334
524
  (value.authorityGeneration !== null &&
335
- (typeof value.authorityGeneration !== 'number' ||
525
+ (typeof value.authorityGeneration !== "number" ||
336
526
  !Number.isSafeInteger(value.authorityGeneration) ||
337
527
  value.authorityGeneration < 0)) ||
338
528
  (value.fencingToken !== null && !nonEmptyString(value.fencingToken)))
339
- throw new Error('authority_result_invalid');
529
+ throw new Error("authority_result_invalid");
340
530
  return Object.freeze({
341
531
  ...value,
342
532
  scope: freezeScope(value.scope),
343
533
  });
344
534
  }
345
535
  function authorityGenerationFor(scope, authority) {
346
- if (authority.status !== 'current')
536
+ if (authority.status !== "current")
347
537
  return null;
348
- return scope.kind === 'coordinated'
538
+ return scope.kind === "coordinated"
349
539
  ? scope.authorityGeneration
350
540
  : (authority.authorityGeneration ?? null);
351
541
  }
352
542
  function fencingTokenFor(scope, authority) {
353
- if (authority.status !== 'current')
543
+ if (authority.status !== "current")
354
544
  return null;
355
- return scope.kind === 'coordinated'
545
+ return scope.kind === "coordinated"
356
546
  ? scope.fencingToken
357
547
  : (authority.fencingToken ?? null);
358
548
  }
@@ -368,7 +558,7 @@ function authorityMatchesReservation(scope, expectedScopeDigest, authority, rese
368
558
  }
369
559
  }
370
560
  function freezeScope(scope) {
371
- if (scope.kind === 'standalone')
561
+ if (scope.kind === "standalone")
372
562
  return Object.freeze({
373
563
  schemaVersion: scope.schemaVersion,
374
564
  kind: scope.kind,
@@ -390,72 +580,72 @@ export function boundedCanonicalControlJsonV1(value, maximumBytes, reasonCode) {
390
580
  if (!Number.isSafeInteger(maximumBytes) ||
391
581
  maximumBytes < 1 ||
392
582
  maximumBytes > MAX_ACTION_INPUT_BYTES_V1)
393
- throw new TypeError('Invalid canonical JSON byte limit');
583
+ throw new TypeError("Invalid canonical JSON byte limit");
394
584
  let nodes = 0;
395
585
  let canonicalBytes = 0;
396
586
  const addCanonicalBytes = (bytes) => {
397
587
  canonicalBytes += bytes;
398
588
  if (canonicalBytes > maximumBytes)
399
- throw new TypeError('Canonical JSON exceeds its byte limit');
589
+ throw new TypeError("Canonical JSON exceeds its byte limit");
400
590
  };
401
591
  const active = new Set();
402
- const stack = [{ kind: 'enter', value, depth: 0 }];
592
+ const stack = [{ kind: "enter", value, depth: 0 }];
403
593
  while (stack.length) {
404
594
  const item = stack.pop();
405
- if (item.kind === 'exit') {
595
+ if (item.kind === "exit") {
406
596
  active.delete(item.value);
407
597
  continue;
408
598
  }
409
599
  nodes += 1;
410
600
  if (nodes > MAX_CONTROL_JSON_NODES_V1)
411
- throw new TypeError('Canonical JSON contains too many nodes');
601
+ throw new TypeError("Canonical JSON contains too many nodes");
412
602
  if (item.depth > MAX_CONTROL_JSON_DEPTH_V1)
413
- throw new TypeError('Canonical JSON is too deeply nested');
603
+ throw new TypeError("Canonical JSON is too deeply nested");
414
604
  if (item.value === null) {
415
605
  addCanonicalBytes(4);
416
606
  continue;
417
607
  }
418
608
  const valueType = typeof item.value;
419
- if (valueType === 'string') {
609
+ if (valueType === "string") {
420
610
  if (item.value.length > maximumBytes)
421
- throw new TypeError('Canonical JSON exceeds its byte limit');
611
+ throw new TypeError("Canonical JSON exceeds its byte limit");
422
612
  addCanonicalBytes(utf8ByteLength(JSON.stringify(item.value)));
423
613
  continue;
424
614
  }
425
- if (valueType === 'boolean') {
615
+ if (valueType === "boolean") {
426
616
  addCanonicalBytes(item.value ? 4 : 5);
427
617
  continue;
428
618
  }
429
- if (valueType === 'number') {
619
+ if (valueType === "number") {
430
620
  if (!Number.isFinite(item.value))
431
- throw new TypeError('Canonical JSON requires finite numbers');
621
+ throw new TypeError("Canonical JSON requires finite numbers");
432
622
  addCanonicalBytes(Object.is(item.value, -0)
433
623
  ? 1
434
624
  : utf8ByteLength(JSON.stringify(item.value)));
435
625
  continue;
436
626
  }
437
- if (valueType !== 'object')
438
- throw new TypeError('Canonical JSON contains a non-JSON value');
627
+ if (valueType !== "object")
628
+ throw new TypeError("Canonical JSON contains a non-JSON value");
439
629
  const objectValue = item.value;
440
630
  if (active.has(objectValue))
441
- throw new TypeError('Canonical JSON cannot contain cycles');
631
+ throw new TypeError("Canonical JSON cannot contain cycles");
442
632
  active.add(objectValue);
443
- stack.push({ kind: 'exit', value: objectValue });
633
+ stack.push({ kind: "exit", value: objectValue });
444
634
  const keys = Reflect.ownKeys(objectValue);
445
- if (keys.some((key) => typeof key === 'symbol'))
446
- throw new TypeError('Canonical JSON cannot contain symbol keys');
635
+ if (keys.some((key) => typeof key === "symbol"))
636
+ throw new TypeError("Canonical JSON cannot contain symbol keys");
447
637
  if (Array.isArray(objectValue)) {
448
638
  if (Object.getPrototypeOf(objectValue) !== Array.prototype)
449
- throw new TypeError('Canonical JSON requires plain arrays');
450
- if (keys.length !== objectValue.length + 1 || !keys.includes('length'))
451
- throw new TypeError('Canonical JSON arrays cannot have extra keys');
639
+ throw new TypeError("Canonical JSON requires plain arrays");
640
+ if (keys.length !== objectValue.length + 1 || !keys.includes("length"))
641
+ throw new TypeError("Canonical JSON arrays cannot have extra keys");
452
642
  addCanonicalBytes(2 + Math.max(0, objectValue.length - 1));
453
643
  for (let index = objectValue.length - 1; index >= 0; index -= 1) {
454
644
  const descriptor = Object.getOwnPropertyDescriptor(objectValue, String(index));
455
- if (!descriptor || !('value' in descriptor) || !descriptor.enumerable)
456
- throw new TypeError('Canonical JSON cannot contain sparse arrays or accessors');
645
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable)
646
+ throw new TypeError("Canonical JSON cannot contain sparse arrays or accessors");
457
647
  stack.push({
458
- kind: 'enter',
648
+ kind: "enter",
459
649
  value: descriptor.value,
460
650
  depth: item.depth + 1,
461
651
  });
@@ -464,18 +654,18 @@ export function boundedCanonicalControlJsonV1(value, maximumBytes, reasonCode) {
464
654
  }
465
655
  const prototype = Object.getPrototypeOf(objectValue);
466
656
  if (prototype !== Object.prototype && prototype !== null)
467
- throw new TypeError('Canonical JSON requires plain objects');
657
+ throw new TypeError("Canonical JSON requires plain objects");
468
658
  addCanonicalBytes(2 + Math.max(0, keys.length - 1));
469
659
  for (let index = keys.length - 1; index >= 0; index -= 1) {
470
660
  const key = keys[index];
471
661
  if (key.length > maximumBytes)
472
- throw new TypeError('Canonical JSON exceeds its byte limit');
662
+ throw new TypeError("Canonical JSON exceeds its byte limit");
473
663
  addCanonicalBytes(utf8ByteLength(JSON.stringify(key)) + 1);
474
664
  const descriptor = Object.getOwnPropertyDescriptor(objectValue, key);
475
- if (!descriptor || !('value' in descriptor) || !descriptor.enumerable)
476
- throw new TypeError('Canonical JSON cannot contain accessors or hidden properties');
665
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable)
666
+ throw new TypeError("Canonical JSON cannot contain accessors or hidden properties");
477
667
  stack.push({
478
- kind: 'enter',
668
+ kind: "enter",
479
669
  value: descriptor.value,
480
670
  depth: item.depth + 1,
481
671
  });
@@ -483,7 +673,7 @@ export function boundedCanonicalControlJsonV1(value, maximumBytes, reasonCode) {
483
673
  }
484
674
  const canonical = canonicalizeControlJsonV1(value);
485
675
  if (utf8ByteLength(canonical) > maximumBytes)
486
- throw new TypeError('Canonical JSON exceeds its byte limit');
676
+ throw new TypeError("Canonical JSON exceeds its byte limit");
487
677
  return canonical;
488
678
  }
489
679
  catch {
@@ -491,13 +681,13 @@ export function boundedCanonicalControlJsonV1(value, maximumBytes, reasonCode) {
491
681
  }
492
682
  }
493
683
  function freezeControlJsonObject(value, maximumBytes) {
494
- return deepFreeze(JSON.parse(boundedCanonicalControlJsonV1(value, maximumBytes, 'action_not_permitted')));
684
+ return deepFreeze(JSON.parse(boundedCanonicalControlJsonV1(value, maximumBytes, "action_not_permitted")));
495
685
  }
496
686
  function freezeToolContext(value) {
497
687
  return deepFreeze(JSON.parse(JSON.stringify(value)));
498
688
  }
499
689
  function deepFreeze(value) {
500
- if (value && typeof value === 'object' && !Object.isFrozen(value)) {
690
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
501
691
  Object.freeze(value);
502
692
  for (const child of Object.values(value))
503
693
  deepFreeze(child);
@@ -505,33 +695,33 @@ function deepFreeze(value) {
505
695
  return value;
506
696
  }
507
697
  function isPlainRecord(value) {
508
- if (!value || typeof value !== 'object' || Array.isArray(value))
698
+ if (!value || typeof value !== "object" || Array.isArray(value))
509
699
  return false;
510
700
  const prototype = Object.getPrototypeOf(value);
511
701
  return ((prototype === Object.prototype || prototype === null) &&
512
- Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => 'value' in descriptor));
702
+ Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => "value" in descriptor));
513
703
  }
514
704
  function isPlainArray(value) {
515
705
  return (Array.isArray(value) &&
516
706
  Object.getPrototypeOf(value) === Array.prototype &&
517
- Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => 'value' in descriptor));
707
+ Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => "value" in descriptor));
518
708
  }
519
709
  function nonEmptyString(value) {
520
- return typeof value === 'string' && value.length > 0;
710
+ return typeof value === "string" && value.length > 0;
521
711
  }
522
712
  function positiveInteger(value) {
523
713
  return Number.isSafeInteger(value) && value > 0;
524
714
  }
525
715
  function isDigest(value) {
526
- return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value);
716
+ return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
527
717
  }
528
718
  function isGrantStatus(value) {
529
- return (value === 'issued' ||
530
- value === 'reserved' ||
531
- value === 'dispatched' ||
532
- value === 'failed' ||
533
- value === 'indeterminate' ||
534
- value === 'expired');
719
+ return (value === "issued" ||
720
+ value === "reserved" ||
721
+ value === "dispatched" ||
722
+ value === "failed" ||
723
+ value === "indeterminate" ||
724
+ value === "expired");
535
725
  }
536
726
  export function isActionScopeV1(value) {
537
727
  if (!isPlainRecord(value) || value.schemaVersion !== 1)
@@ -543,44 +733,44 @@ export function isActionScopeV1(value) {
543
733
  positiveInteger(value.policyVersion);
544
734
  if (!common)
545
735
  return false;
546
- if (value.kind === 'standalone')
736
+ if (value.kind === "standalone")
547
737
  return (hasExactKeys(value, [
548
- 'schemaVersion',
549
- 'kind',
550
- 'tenantId',
551
- 'runId',
552
- 'agentId',
553
- 'organizationId',
554
- 'workspaceId',
555
- 'policyId',
556
- 'policyVersion',
738
+ "schemaVersion",
739
+ "kind",
740
+ "tenantId",
741
+ "runId",
742
+ "agentId",
743
+ "organizationId",
744
+ "workspaceId",
745
+ "policyId",
746
+ "policyVersion",
557
747
  ]) &&
558
- (typeof value.organizationId === 'string' ||
748
+ (typeof value.organizationId === "string" ||
559
749
  value.organizationId === null) &&
560
- (typeof value.workspaceId === 'string' || value.workspaceId === null));
561
- return (value.kind === 'coordinated' &&
750
+ (typeof value.workspaceId === "string" || value.workspaceId === null));
751
+ return (value.kind === "coordinated" &&
562
752
  hasExactKeys(value, [
563
- 'schemaVersion',
564
- 'kind',
565
- 'tenantId',
566
- 'runId',
567
- 'agentId',
568
- 'policyId',
569
- 'policyVersion',
570
- 'meshId',
571
- 'objectiveId',
572
- 'objectiveRevision',
573
- 'workItemId',
574
- 'workItemRevision',
575
- 'peerId',
576
- 'instanceId',
577
- 'assignmentAuthorityId',
578
- 'assignmentEpoch',
579
- 'fencingToken',
580
- 'leaseExpiresAtLogicalMs',
581
- 'authorityGeneration',
582
- 'objectiveTerminal',
583
- 'workTerminal',
753
+ "schemaVersion",
754
+ "kind",
755
+ "tenantId",
756
+ "runId",
757
+ "agentId",
758
+ "policyId",
759
+ "policyVersion",
760
+ "meshId",
761
+ "objectiveId",
762
+ "objectiveRevision",
763
+ "workItemId",
764
+ "workItemRevision",
765
+ "peerId",
766
+ "instanceId",
767
+ "assignmentAuthorityId",
768
+ "assignmentEpoch",
769
+ "fencingToken",
770
+ "leaseExpiresAtLogicalMs",
771
+ "authorityGeneration",
772
+ "objectiveTerminal",
773
+ "workTerminal",
584
774
  ]) &&
585
775
  [
586
776
  value.meshId,
@@ -597,9 +787,9 @@ export function isActionScopeV1(value) {
597
787
  value.assignmentEpoch,
598
788
  value.leaseExpiresAtLogicalMs,
599
789
  value.authorityGeneration,
600
- ].every((item) => typeof item === 'number' && Number.isSafeInteger(item) && item >= 0) &&
601
- typeof value.objectiveTerminal === 'boolean' &&
602
- typeof value.workTerminal === 'boolean');
790
+ ].every((item) => typeof item === "number" && Number.isSafeInteger(item) && item >= 0) &&
791
+ typeof value.objectiveTerminal === "boolean" &&
792
+ typeof value.workTerminal === "boolean");
603
793
  }
604
794
  function hasExactKeys(value, expected) {
605
795
  const actual = Object.getOwnPropertyNames(value).sort();
@@ -620,12 +810,12 @@ export class ActionGateway {
620
810
  this.ledger = ledger;
621
811
  if (!nonEmptyString(authorityResolver.resolverId) ||
622
812
  !positiveInteger(authorityResolver.resolverVersion))
623
- throw new Error('dependency_rebind_failed');
813
+ throw new Error("dependency_rebind_failed");
624
814
  const maxActionInputBytes = limits.maxActionInputBytes ?? MAX_ACTION_INPUT_BYTES_V1;
625
815
  if (!Number.isSafeInteger(maxActionInputBytes) ||
626
816
  maxActionInputBytes < 1 ||
627
817
  maxActionInputBytes > MAX_ACTION_INPUT_BYTES_V1)
628
- throw new Error('dependency_rebind_failed');
818
+ throw new Error("dependency_rebind_failed");
629
819
  this.maxActionInputBytes = maxActionInputBytes;
630
820
  this.binding = freezeBinding(binding);
631
821
  this.dispatcher = Object.freeze({
@@ -652,17 +842,18 @@ export class ActionGateway {
652
842
  }
653
843
  async invoke(input) {
654
844
  if (!isPlainRecord(input) ||
655
- !hasExactKeys(input, Object.hasOwn(input, 'input')
656
- ? ['schemaVersion', 'grantId', 'input', 'logicalTimeMs']
657
- : ['schemaVersion', 'grantId', 'logicalTimeMs']) ||
845
+ !hasExactKeys(input, Object.hasOwn(input, "input")
846
+ ? ["schemaVersion", "grantId", "input", "logicalTimeMs"]
847
+ : ["schemaVersion", "grantId", "logicalTimeMs"]) ||
658
848
  input.schemaVersion !== 1 ||
659
849
  !nonEmptyString(input.grantId))
660
- throw new Error('action_not_permitted');
850
+ throw new Error("action_not_permitted");
661
851
  const value = freezeControlJsonObject(input.input ?? {}, this.maxActionInputBytes);
662
852
  const digest = actionInputDigest(value);
663
- const grant = this.ledger.get(input.grantId);
853
+ const grant = await this.ledger.loadGrant(input.grantId);
664
854
  if (!grant)
665
- throw new Error('grant_missing');
855
+ throw new Error("grant_missing");
856
+ assertGrantSnapshot(grant, Number.MAX_SAFE_INTEGER);
666
857
  if (grant.actionBindingId !== this.binding.actionBindingId ||
667
858
  grant.actionBindingVersion !== this.binding.actionBindingVersion ||
668
859
  grant.namespace !== this.binding.namespace ||
@@ -678,26 +869,26 @@ export class ActionGateway {
678
869
  this.binding.fencingMode !== this.dispatcher.fencingMode ||
679
870
  grant.inputDigest !== digest ||
680
871
  grant.actionDigest !== actionDigest(grant, this.binding))
681
- throw new Error('grant_action_mismatch');
682
- if (grant.scope.kind === 'coordinated' &&
683
- (this.binding.fencingMode !== 'downstream_atomic' ||
684
- this.dispatcher.fencingMode !== 'downstream_atomic'))
685
- throw new Error('dependency_rebind_failed');
872
+ throw new Error("grant_action_mismatch");
873
+ if (grant.scope.kind === "coordinated" &&
874
+ (this.binding.fencingMode !== "downstream_atomic" ||
875
+ this.dispatcher.fencingMode !== "downstream_atomic"))
876
+ throw new Error("dependency_rebind_failed");
686
877
  if (!(await this.assessmentResolver.consumeCurrent(grant, input.logicalTimeMs)))
687
- throw new Error('grant_assessment_mismatch');
878
+ throw new Error("grant_assessment_mismatch");
688
879
  const first = normalizeAuthorityResultV1(this.authorityResolver, grant.scope, grant.actionDigest, await this.authorityResolver.resolve(grant.scope, grant.actionDigest, input.logicalTimeMs));
689
- const reserved = reserveGrantForGateway(this.ledger, grant.grantId, this.ledger.gatewayId, input.logicalTimeMs, first);
880
+ const reserved = await reserveGrantForGateway(this.ledger, grant.grantId, this.ledger.gatewayId, input.logicalTimeMs, first);
690
881
  const reservation = reserved.reservation;
691
882
  try {
692
883
  const context = freezeToolContext(await this.contextResolver.resolve(reserved.scope, this.binding));
693
884
  if (context.tenant.tenantId !== reserved.scope.tenantId ||
694
885
  context.toolId !== this.binding.toolId ||
695
886
  context.runId !== reserved.scope.runId)
696
- throw new Error('invocation_context_mismatch');
887
+ throw new Error("invocation_context_mismatch");
697
888
  const latest = normalizeAuthorityResultV1(this.authorityResolver, reserved.scope, reserved.actionDigest, await this.authorityResolver.resolve(reserved.scope, reserved.actionDigest, input.logicalTimeMs));
698
889
  if (!authorityMatchesReservation(reserved.scope, reserved.scopeDigest, latest, reservation)) {
699
- settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, 'failed');
700
- throw new Error('grant_fence_stale');
890
+ await settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, "failed");
891
+ throw new Error("grant_fence_stale");
701
892
  }
702
893
  let assessmentStillCurrent = false;
703
894
  try {
@@ -707,8 +898,8 @@ export class ActionGateway {
707
898
  assessmentStillCurrent = false;
708
899
  }
709
900
  if (!assessmentStillCurrent) {
710
- settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, 'failed');
711
- throw new Error('grant_assessment_mismatch');
901
+ await settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, "failed");
902
+ throw new Error("grant_assessment_mismatch");
712
903
  }
713
904
  const permit = Object.freeze({
714
905
  schemaVersion: 1,
@@ -728,13 +919,13 @@ export class ActionGateway {
728
919
  context,
729
920
  permit,
730
921
  });
731
- settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, result.ok ? 'dispatched' : 'failed');
922
+ await settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, result.ok ? "dispatched" : "failed");
732
923
  return result;
733
924
  }
734
925
  catch (error) {
735
- const current = this.ledger.get(reserved.grantId);
736
- if (current?.status === 'reserved')
737
- settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, 'indeterminate');
926
+ const current = await this.ledger.loadGrant(reserved.grantId);
927
+ if (current?.status === "reserved")
928
+ await settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, "indeterminate");
738
929
  throw error;
739
930
  }
740
931
  }
@@ -742,8 +933,8 @@ export class ActionGateway {
742
933
  export async function freezeToolRegistryBinding(registry, toolId, binding) {
743
934
  const found = await registry.get(toolId);
744
935
  if (!found)
745
- throw new Error('action_not_permitted');
746
- const handlerDigest = controlDigest('handler-binding', {
936
+ throw new Error("action_not_permitted");
937
+ const handlerDigest = controlDigest("handler-binding", {
747
938
  definition: found.definition,
748
939
  binding: { ...binding, toolId },
749
940
  });