@agentplat/inference-control 0.3.0-alpha.3

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.
Files changed (83) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +101 -0
  3. package/dist/assessments.d.ts +5 -0
  4. package/dist/assessments.d.ts.map +1 -0
  5. package/dist/assessments.js +216 -0
  6. package/dist/assessments.js.map +1 -0
  7. package/dist/boundary.d.ts +24 -0
  8. package/dist/boundary.d.ts.map +1 -0
  9. package/dist/boundary.js +46 -0
  10. package/dist/boundary.js.map +1 -0
  11. package/dist/canonical.d.ts +6 -0
  12. package/dist/canonical.d.ts.map +1 -0
  13. package/dist/canonical.js +74 -0
  14. package/dist/canonical.js.map +1 -0
  15. package/dist/capabilities.d.ts +43 -0
  16. package/dist/capabilities.d.ts.map +1 -0
  17. package/dist/capabilities.js +304 -0
  18. package/dist/capabilities.js.map +1 -0
  19. package/dist/context.d.ts +7 -0
  20. package/dist/context.d.ts.map +1 -0
  21. package/dist/context.js +191 -0
  22. package/dist/context.js.map +1 -0
  23. package/dist/index.d.ts +14 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +14 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/messages.d.ts +124 -0
  28. package/dist/messages.d.ts.map +1 -0
  29. package/dist/messages.js +475 -0
  30. package/dist/messages.js.map +1 -0
  31. package/dist/model.d.ts +104 -0
  32. package/dist/model.d.ts.map +1 -0
  33. package/dist/model.js +437 -0
  34. package/dist/model.js.map +1 -0
  35. package/dist/policy.d.ts +11 -0
  36. package/dist/policy.d.ts.map +1 -0
  37. package/dist/policy.js +338 -0
  38. package/dist/policy.js.map +1 -0
  39. package/dist/reducer.d.ts +193 -0
  40. package/dist/reducer.d.ts.map +1 -0
  41. package/dist/reducer.js +1341 -0
  42. package/dist/reducer.js.map +1 -0
  43. package/dist/runtime.d.ts +132 -0
  44. package/dist/runtime.d.ts.map +1 -0
  45. package/dist/runtime.js +587 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/dist/scopes.d.ts +3 -0
  48. package/dist/scopes.d.ts.map +1 -0
  49. package/dist/scopes.js +83 -0
  50. package/dist/scopes.js.map +1 -0
  51. package/dist/sha256.d.ts +2 -0
  52. package/dist/sha256.d.ts.map +1 -0
  53. package/dist/sha256.js +72 -0
  54. package/dist/sha256.js.map +1 -0
  55. package/dist/state-records.d.ts +8 -0
  56. package/dist/state-records.d.ts.map +1 -0
  57. package/dist/state-records.js +204 -0
  58. package/dist/state-records.js.map +1 -0
  59. package/dist/state.d.ts +19 -0
  60. package/dist/state.d.ts.map +1 -0
  61. package/dist/state.js +689 -0
  62. package/dist/state.js.map +1 -0
  63. package/dist/streams.d.ts +5 -0
  64. package/dist/streams.d.ts.map +1 -0
  65. package/dist/streams.js +93 -0
  66. package/dist/streams.js.map +1 -0
  67. package/dist/telemetry.d.ts +12 -0
  68. package/dist/telemetry.d.ts.map +1 -0
  69. package/dist/telemetry.js +20 -0
  70. package/dist/telemetry.js.map +1 -0
  71. package/dist/tools.d.ts +221 -0
  72. package/dist/tools.d.ts.map +1 -0
  73. package/dist/tools.js +755 -0
  74. package/dist/tools.js.map +1 -0
  75. package/dist/types.d.ts +474 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +43 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/validation.d.ts +23 -0
  80. package/dist/validation.d.ts.map +1 -0
  81. package/dist/validation.js +154 -0
  82. package/dist/validation.js.map +1 -0
  83. package/package.json +56 -0
package/dist/tools.js ADDED
@@ -0,0 +1,755 @@
1
+ import { canonicalizeControlJsonV1, digestControlJsonV1, utf8ByteLength, } from './canonical.js';
2
+ export const MAX_ACTION_INPUT_BYTES_V1 = 65_536;
3
+ export const MAX_CONTROL_JSON_DEPTH_V1 = 64;
4
+ export const MAX_CONTROL_JSON_NODES_V1 = 65_536;
5
+ export function canonicalControlJson(value) {
6
+ return canonicalizeControlJsonV1(value);
7
+ }
8
+ export function controlDigest(domain, value) {
9
+ if (domain === 'grant')
10
+ return digestControlJsonV1('state', value);
11
+ return digestControlJsonV1(domain, value);
12
+ }
13
+ export function actionInputDigest(input) {
14
+ return controlDigest('action-input', input);
15
+ }
16
+ export function scopeDigest(scope) {
17
+ return controlDigest('scope', scope);
18
+ }
19
+ export function actionDigest(grant, binding) {
20
+ return controlDigest('action', {
21
+ scopeDigest: grant.scopeDigest,
22
+ namespace: binding.namespace,
23
+ toolId: binding.toolId,
24
+ operation: binding.operation,
25
+ actionBindingId: binding.actionBindingId,
26
+ actionBindingVersion: binding.actionBindingVersion,
27
+ handlerDigest: binding.handlerDigest,
28
+ inputDigest: grant.inputDigest,
29
+ });
30
+ }
31
+ function freezeGrant(grant) {
32
+ return Object.freeze({
33
+ ...grant,
34
+ scope: freezeScope(grant.scope),
35
+ reservation: grant.reservation
36
+ ? Object.freeze({ ...grant.reservation })
37
+ : null,
38
+ });
39
+ }
40
+ function idempotencyKey(scope, key) {
41
+ return `${scope.length}:${scope}${key.length}:${key}`;
42
+ }
43
+ function assertTime(time) {
44
+ if (!Number.isSafeInteger(time) || time < 0)
45
+ throw new TypeError('logicalTimeMs must be a non-negative safe integer');
46
+ }
47
+ const grantReserveCapability = Symbol('grant-ledger-reserve-capability');
48
+ const grantOutcomeCapability = Symbol('grant-ledger-outcome-capability');
49
+ /** In-memory, single-process ledger. Reservation is synchronous and linearized. */
50
+ export class LocalGrantLedger {
51
+ gatewayId;
52
+ grants = new Map();
53
+ idempotency = new Map();
54
+ highWater = 0;
55
+ constructor(gatewayId) {
56
+ this.gatewayId = gatewayId;
57
+ }
58
+ issue(grant) {
59
+ this.time(grant.issuedAtLogicalMs);
60
+ if (grant.schemaVersion !== 1 ||
61
+ grant.expiresAtLogicalMs <= grant.issuedAtLogicalMs ||
62
+ grant.expiresAtLogicalMs - grant.issuedAtLogicalMs > 120_000 ||
63
+ !grant.singleUse ||
64
+ grant.status !== 'issued' ||
65
+ grant.reservation !== null ||
66
+ !isActionScopeV1(grant.scope) ||
67
+ scopeDigest(grant.scope) !== grant.scopeDigest ||
68
+ !/^sha256:[0-9a-f]{64}$/.test(grant.inputDigest) ||
69
+ !/^sha256:[0-9a-f]{64}$/.test(grant.actionDigest))
70
+ throw new TypeError('Invalid Action Grant');
71
+ const prior = this.grants.get(grant.grantId);
72
+ if (prior) {
73
+ if (controlDigest('grant', prior) !==
74
+ controlDigest('grant', grant))
75
+ throw new Error('state_conflict');
76
+ return prior;
77
+ }
78
+ const key = idempotencyKey(grant.scopeDigest, grant.idempotencyKey);
79
+ const previous = this.idempotency.get(key);
80
+ if (previous) {
81
+ if (previous.actionDigest !== grant.actionDigest)
82
+ throw new Error('grant_idempotency_conflict');
83
+ return this.grants.get(previous.grantId);
84
+ }
85
+ const retained = freezeGrant(grant);
86
+ this.grants.set(retained.grantId, retained);
87
+ this.idempotency.set(key, Object.freeze({
88
+ schemaVersion: 1,
89
+ scopeDigest: retained.scopeDigest,
90
+ idempotencyKey: retained.idempotencyKey,
91
+ actionDigest: retained.actionDigest,
92
+ grantId: retained.grantId,
93
+ retainedOutcome: retained.status,
94
+ }));
95
+ return retained;
96
+ }
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
+ snapshot() {
135
+ return Object.freeze({
136
+ schemaVersion: 1,
137
+ highWaterLogicalMs: this.highWater,
138
+ grants: Object.freeze([...this.grants.values()].map((grant) => freezeGrant(grant))),
139
+ });
140
+ }
141
+ restore(snapshot) {
142
+ if (!snapshot ||
143
+ !isPlainRecord(snapshot) ||
144
+ snapshot.schemaVersion !== 1 ||
145
+ !isPlainArray(snapshot.grants) ||
146
+ !Number.isSafeInteger(snapshot.highWaterLogicalMs) ||
147
+ snapshot.highWaterLogicalMs < 0 ||
148
+ snapshot.highWaterLogicalMs < this.highWater)
149
+ throw new TypeError('Invalid Action Grant snapshot');
150
+ const grants = new Map();
151
+ const idempotency = new Map();
152
+ for (const source of snapshot.grants) {
153
+ assertGrantSnapshot(source, snapshot.highWaterLogicalMs);
154
+ const grant = source.status === 'reserved'
155
+ ? {
156
+ ...source,
157
+ status: 'indeterminate',
158
+ stateGeneration: source.stateGeneration + 1,
159
+ }
160
+ : source;
161
+ const frozen = freezeGrant(grant);
162
+ if (grants.has(frozen.grantId))
163
+ throw new Error('state_conflict');
164
+ const key = idempotencyKey(frozen.scopeDigest, frozen.idempotencyKey);
165
+ const previous = idempotency.get(key);
166
+ if (previous && previous.actionDigest !== frozen.actionDigest)
167
+ throw new Error('grant_idempotency_conflict');
168
+ if (previous)
169
+ throw new Error('state_conflict');
170
+ grants.set(frozen.grantId, frozen);
171
+ idempotency.set(key, Object.freeze({
172
+ schemaVersion: 1,
173
+ scopeDigest: grant.scopeDigest,
174
+ idempotencyKey: grant.idempotencyKey,
175
+ actionDigest: grant.actionDigest,
176
+ grantId: grant.grantId,
177
+ retainedOutcome: grant.status,
178
+ }));
179
+ }
180
+ this.grants.clear();
181
+ this.idempotency.clear();
182
+ for (const [id, grant] of grants)
183
+ this.grants.set(id, grant);
184
+ for (const [key, record] of idempotency)
185
+ this.idempotency.set(key, record);
186
+ this.highWater = snapshot.highWaterLogicalMs;
187
+ }
188
+ get(grantId) {
189
+ return this.grants.get(grantId);
190
+ }
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;
203
+ }
204
+ expire(grant) {
205
+ return this.replace({
206
+ ...grant,
207
+ stateGeneration: grant.stateGeneration + 1,
208
+ status: 'expired',
209
+ });
210
+ }
211
+ require(id) {
212
+ const grant = this.grants.get(id);
213
+ if (!grant)
214
+ throw new Error('grant_missing');
215
+ return grant;
216
+ }
217
+ time(value) {
218
+ assertTime(value);
219
+ if (value < this.highWater)
220
+ throw new Error('logical_time_rollback');
221
+ this.highWater = value;
222
+ }
223
+ }
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);
230
+ }
231
+ function assertGrantSnapshot(grant, highWater) {
232
+ if (!isPlainRecord(grant) ||
233
+ grant.schemaVersion !== 1 ||
234
+ !nonEmptyString(grant.grantId) ||
235
+ !positiveInteger(grant.stateGeneration) ||
236
+ !isActionScopeV1(grant.scope) ||
237
+ scopeDigest(grant.scope) !== grant.scopeDigest ||
238
+ !isDigest(grant.inputDigest) ||
239
+ !isDigest(grant.actionDigest) ||
240
+ !isDigest(grant.handlerDigest) ||
241
+ !Number.isSafeInteger(grant.issuedAtLogicalMs) ||
242
+ grant.issuedAtLogicalMs < 0 ||
243
+ grant.issuedAtLogicalMs > highWater ||
244
+ !Number.isSafeInteger(grant.expiresAtLogicalMs) ||
245
+ grant.expiresAtLogicalMs <= grant.issuedAtLogicalMs ||
246
+ grant.expiresAtLogicalMs - grant.issuedAtLogicalMs > 120_000 ||
247
+ !grant.singleUse ||
248
+ !isGrantStatus(grant.status) ||
249
+ !nonEmptyString(grant.idempotencyKey) ||
250
+ !nonEmptyString(grant.scopeDigest))
251
+ throw new TypeError('Invalid Action Grant snapshot');
252
+ if (grant.status === 'issued' || grant.status === 'expired') {
253
+ if (grant.reservation !== null)
254
+ throw new TypeError('Invalid Action Grant snapshot');
255
+ }
256
+ else if (!isActionReservation(grant.reservation, highWater, grant)) {
257
+ throw new TypeError('Invalid Action Grant snapshot');
258
+ }
259
+ }
260
+ function isActionReservation(value, highWater, grant) {
261
+ return Boolean(value &&
262
+ isPlainRecord(value) &&
263
+ value.schemaVersion === 1 &&
264
+ nonEmptyString(value.reservationId) &&
265
+ nonEmptyString(value.dispatchAttemptId) &&
266
+ nonEmptyString(value.reservedByGatewayId) &&
267
+ positiveInteger(value.reservedStateGeneration) &&
268
+ value.reservedStateGeneration <= grant.stateGeneration &&
269
+ Number.isSafeInteger(value.reservedAtLogicalMs) &&
270
+ value.reservedAtLogicalMs >= grant.issuedAtLogicalMs &&
271
+ value.reservedAtLogicalMs <= highWater &&
272
+ (value.authorityGeneration === null ||
273
+ (Number.isSafeInteger(value.authorityGeneration) &&
274
+ value.authorityGeneration >= 0)) &&
275
+ (value.fencingToken === null || nonEmptyString(value.fencingToken)));
276
+ }
277
+ function assertCurrentAuthority(scope, expectedScopeDigest, authority) {
278
+ if (authority.status !== 'current' ||
279
+ !isActionScopeV1(authority.scope) ||
280
+ scopeDigest(authority.scope) !== expectedScopeDigest)
281
+ throw new Error('grant_scope_mismatch');
282
+ if (scope.kind === 'coordinated' &&
283
+ (authority.authorityGeneration !== scope.authorityGeneration ||
284
+ authority.fencingToken !== scope.fencingToken))
285
+ throw new Error('grant_fence_stale');
286
+ if (scope.kind === 'standalone' &&
287
+ ((authority.authorityGeneration != null) !==
288
+ (authority.fencingToken != null) ||
289
+ (authority.authorityGeneration != null &&
290
+ (!Number.isSafeInteger(authority.authorityGeneration) ||
291
+ authority.authorityGeneration < 0)) ||
292
+ (authority.fencingToken != null &&
293
+ !nonEmptyString(authority.fencingToken))))
294
+ throw new Error('grant_fence_stale');
295
+ }
296
+ export function normalizeAuthorityResultV1(resolver, scope, actionDigestValue, value) {
297
+ if (!isPlainRecord(value) || value.schemaVersion !== 1)
298
+ throw new Error('authority_result_invalid');
299
+ const expectedScopeDigest = scopeDigest(scope);
300
+ const common = value.resolverId === resolver.resolverId &&
301
+ value.resolverVersion === resolver.resolverVersion &&
302
+ value.scopeDigest === expectedScopeDigest &&
303
+ value.actionDigest === actionDigestValue &&
304
+ isDigest(value.scopeDigest) &&
305
+ isDigest(value.actionDigest);
306
+ if (!common)
307
+ throw new Error('authority_result_invalid');
308
+ if (value.status === 'stale' || value.status === 'unavailable') {
309
+ if (!hasExactKeys(value, [
310
+ 'schemaVersion',
311
+ 'status',
312
+ 'resolverId',
313
+ 'resolverVersion',
314
+ 'scopeDigest',
315
+ 'actionDigest',
316
+ ]))
317
+ throw new Error('authority_result_invalid');
318
+ return Object.freeze({ ...value });
319
+ }
320
+ if (value.status !== 'current' ||
321
+ !hasExactKeys(value, [
322
+ 'schemaVersion',
323
+ 'status',
324
+ 'resolverId',
325
+ 'resolverVersion',
326
+ 'scopeDigest',
327
+ 'actionDigest',
328
+ 'scope',
329
+ 'authorityGeneration',
330
+ 'fencingToken',
331
+ ]) ||
332
+ !isActionScopeV1(value.scope) ||
333
+ scopeDigest(value.scope) !== expectedScopeDigest ||
334
+ (value.authorityGeneration !== null &&
335
+ (typeof value.authorityGeneration !== 'number' ||
336
+ !Number.isSafeInteger(value.authorityGeneration) ||
337
+ value.authorityGeneration < 0)) ||
338
+ (value.fencingToken !== null && !nonEmptyString(value.fencingToken)))
339
+ throw new Error('authority_result_invalid');
340
+ return Object.freeze({
341
+ ...value,
342
+ scope: freezeScope(value.scope),
343
+ });
344
+ }
345
+ function authorityGenerationFor(scope, authority) {
346
+ if (authority.status !== 'current')
347
+ return null;
348
+ return scope.kind === 'coordinated'
349
+ ? scope.authorityGeneration
350
+ : (authority.authorityGeneration ?? null);
351
+ }
352
+ function fencingTokenFor(scope, authority) {
353
+ if (authority.status !== 'current')
354
+ return null;
355
+ return scope.kind === 'coordinated'
356
+ ? scope.fencingToken
357
+ : (authority.fencingToken ?? null);
358
+ }
359
+ function authorityMatchesReservation(scope, expectedScopeDigest, authority, reservation) {
360
+ try {
361
+ assertCurrentAuthority(scope, expectedScopeDigest, authority);
362
+ return (authorityGenerationFor(scope, authority) ===
363
+ reservation.authorityGeneration &&
364
+ fencingTokenFor(scope, authority) === reservation.fencingToken);
365
+ }
366
+ catch {
367
+ return false;
368
+ }
369
+ }
370
+ function freezeScope(scope) {
371
+ if (scope.kind === 'standalone')
372
+ return Object.freeze({
373
+ schemaVersion: scope.schemaVersion,
374
+ kind: scope.kind,
375
+ tenantId: scope.tenantId,
376
+ runId: scope.runId,
377
+ agentId: scope.agentId,
378
+ organizationId: scope.organizationId,
379
+ workspaceId: scope.workspaceId,
380
+ policyId: scope.policyId,
381
+ policyVersion: scope.policyVersion,
382
+ });
383
+ return Object.freeze({ ...scope });
384
+ }
385
+ function freezeBinding(binding) {
386
+ return Object.freeze({ ...binding });
387
+ }
388
+ export function boundedCanonicalControlJsonV1(value, maximumBytes, reasonCode) {
389
+ try {
390
+ if (!Number.isSafeInteger(maximumBytes) ||
391
+ maximumBytes < 1 ||
392
+ maximumBytes > MAX_ACTION_INPUT_BYTES_V1)
393
+ throw new TypeError('Invalid canonical JSON byte limit');
394
+ let nodes = 0;
395
+ let canonicalBytes = 0;
396
+ const addCanonicalBytes = (bytes) => {
397
+ canonicalBytes += bytes;
398
+ if (canonicalBytes > maximumBytes)
399
+ throw new TypeError('Canonical JSON exceeds its byte limit');
400
+ };
401
+ const active = new Set();
402
+ const stack = [{ kind: 'enter', value, depth: 0 }];
403
+ while (stack.length) {
404
+ const item = stack.pop();
405
+ if (item.kind === 'exit') {
406
+ active.delete(item.value);
407
+ continue;
408
+ }
409
+ nodes += 1;
410
+ if (nodes > MAX_CONTROL_JSON_NODES_V1)
411
+ throw new TypeError('Canonical JSON contains too many nodes');
412
+ if (item.depth > MAX_CONTROL_JSON_DEPTH_V1)
413
+ throw new TypeError('Canonical JSON is too deeply nested');
414
+ if (item.value === null) {
415
+ addCanonicalBytes(4);
416
+ continue;
417
+ }
418
+ const valueType = typeof item.value;
419
+ if (valueType === 'string') {
420
+ if (item.value.length > maximumBytes)
421
+ throw new TypeError('Canonical JSON exceeds its byte limit');
422
+ addCanonicalBytes(utf8ByteLength(JSON.stringify(item.value)));
423
+ continue;
424
+ }
425
+ if (valueType === 'boolean') {
426
+ addCanonicalBytes(item.value ? 4 : 5);
427
+ continue;
428
+ }
429
+ if (valueType === 'number') {
430
+ if (!Number.isFinite(item.value))
431
+ throw new TypeError('Canonical JSON requires finite numbers');
432
+ addCanonicalBytes(Object.is(item.value, -0)
433
+ ? 1
434
+ : utf8ByteLength(JSON.stringify(item.value)));
435
+ continue;
436
+ }
437
+ if (valueType !== 'object')
438
+ throw new TypeError('Canonical JSON contains a non-JSON value');
439
+ const objectValue = item.value;
440
+ if (active.has(objectValue))
441
+ throw new TypeError('Canonical JSON cannot contain cycles');
442
+ active.add(objectValue);
443
+ stack.push({ kind: 'exit', value: objectValue });
444
+ const keys = Reflect.ownKeys(objectValue);
445
+ if (keys.some((key) => typeof key === 'symbol'))
446
+ throw new TypeError('Canonical JSON cannot contain symbol keys');
447
+ if (Array.isArray(objectValue)) {
448
+ 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');
452
+ addCanonicalBytes(2 + Math.max(0, objectValue.length - 1));
453
+ for (let index = objectValue.length - 1; index >= 0; index -= 1) {
454
+ 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');
457
+ stack.push({
458
+ kind: 'enter',
459
+ value: descriptor.value,
460
+ depth: item.depth + 1,
461
+ });
462
+ }
463
+ continue;
464
+ }
465
+ const prototype = Object.getPrototypeOf(objectValue);
466
+ if (prototype !== Object.prototype && prototype !== null)
467
+ throw new TypeError('Canonical JSON requires plain objects');
468
+ addCanonicalBytes(2 + Math.max(0, keys.length - 1));
469
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
470
+ const key = keys[index];
471
+ if (key.length > maximumBytes)
472
+ throw new TypeError('Canonical JSON exceeds its byte limit');
473
+ addCanonicalBytes(utf8ByteLength(JSON.stringify(key)) + 1);
474
+ 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');
477
+ stack.push({
478
+ kind: 'enter',
479
+ value: descriptor.value,
480
+ depth: item.depth + 1,
481
+ });
482
+ }
483
+ }
484
+ const canonical = canonicalizeControlJsonV1(value);
485
+ if (utf8ByteLength(canonical) > maximumBytes)
486
+ throw new TypeError('Canonical JSON exceeds its byte limit');
487
+ return canonical;
488
+ }
489
+ catch {
490
+ throw new Error(reasonCode);
491
+ }
492
+ }
493
+ function freezeControlJsonObject(value, maximumBytes) {
494
+ return deepFreeze(JSON.parse(boundedCanonicalControlJsonV1(value, maximumBytes, 'action_not_permitted')));
495
+ }
496
+ function freezeToolContext(value) {
497
+ return deepFreeze(JSON.parse(JSON.stringify(value)));
498
+ }
499
+ function deepFreeze(value) {
500
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
501
+ Object.freeze(value);
502
+ for (const child of Object.values(value))
503
+ deepFreeze(child);
504
+ }
505
+ return value;
506
+ }
507
+ function isPlainRecord(value) {
508
+ if (!value || typeof value !== 'object' || Array.isArray(value))
509
+ return false;
510
+ const prototype = Object.getPrototypeOf(value);
511
+ return ((prototype === Object.prototype || prototype === null) &&
512
+ Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => 'value' in descriptor));
513
+ }
514
+ function isPlainArray(value) {
515
+ return (Array.isArray(value) &&
516
+ Object.getPrototypeOf(value) === Array.prototype &&
517
+ Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => 'value' in descriptor));
518
+ }
519
+ function nonEmptyString(value) {
520
+ return typeof value === 'string' && value.length > 0;
521
+ }
522
+ function positiveInteger(value) {
523
+ return Number.isSafeInteger(value) && value > 0;
524
+ }
525
+ function isDigest(value) {
526
+ return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value);
527
+ }
528
+ function isGrantStatus(value) {
529
+ return (value === 'issued' ||
530
+ value === 'reserved' ||
531
+ value === 'dispatched' ||
532
+ value === 'failed' ||
533
+ value === 'indeterminate' ||
534
+ value === 'expired');
535
+ }
536
+ export function isActionScopeV1(value) {
537
+ if (!isPlainRecord(value) || value.schemaVersion !== 1)
538
+ return false;
539
+ const common = nonEmptyString(value.tenantId) &&
540
+ nonEmptyString(value.runId) &&
541
+ nonEmptyString(value.agentId) &&
542
+ nonEmptyString(value.policyId) &&
543
+ positiveInteger(value.policyVersion);
544
+ if (!common)
545
+ return false;
546
+ if (value.kind === 'standalone')
547
+ return (hasExactKeys(value, [
548
+ 'schemaVersion',
549
+ 'kind',
550
+ 'tenantId',
551
+ 'runId',
552
+ 'agentId',
553
+ 'organizationId',
554
+ 'workspaceId',
555
+ 'policyId',
556
+ 'policyVersion',
557
+ ]) &&
558
+ (typeof value.organizationId === 'string' ||
559
+ value.organizationId === null) &&
560
+ (typeof value.workspaceId === 'string' || value.workspaceId === null));
561
+ return (value.kind === 'coordinated' &&
562
+ 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',
584
+ ]) &&
585
+ [
586
+ value.meshId,
587
+ value.objectiveId,
588
+ value.workItemId,
589
+ value.peerId,
590
+ value.instanceId,
591
+ value.assignmentAuthorityId,
592
+ value.fencingToken,
593
+ ].every(nonEmptyString) &&
594
+ [
595
+ value.objectiveRevision,
596
+ value.workItemRevision,
597
+ value.assignmentEpoch,
598
+ value.leaseExpiresAtLogicalMs,
599
+ value.authorityGeneration,
600
+ ].every((item) => typeof item === 'number' && Number.isSafeInteger(item) && item >= 0) &&
601
+ typeof value.objectiveTerminal === 'boolean' &&
602
+ typeof value.workTerminal === 'boolean');
603
+ }
604
+ function hasExactKeys(value, expected) {
605
+ const actual = Object.getOwnPropertyNames(value).sort();
606
+ const sorted = [...expected].sort();
607
+ return (Object.getOwnPropertySymbols(value).length === 0 &&
608
+ actual.length === sorted.length &&
609
+ actual.every((key, index) => key === sorted[index]));
610
+ }
611
+ export class ActionGateway {
612
+ ledger;
613
+ binding;
614
+ dispatcher;
615
+ contextResolver;
616
+ authorityResolver;
617
+ assessmentResolver;
618
+ maxActionInputBytes;
619
+ constructor(ledger, binding, dispatcher, contextResolver, authorityResolver, assessmentResolver, limits = {}) {
620
+ this.ledger = ledger;
621
+ if (!nonEmptyString(authorityResolver.resolverId) ||
622
+ !positiveInteger(authorityResolver.resolverVersion))
623
+ throw new Error('dependency_rebind_failed');
624
+ const maxActionInputBytes = limits.maxActionInputBytes ?? MAX_ACTION_INPUT_BYTES_V1;
625
+ if (!Number.isSafeInteger(maxActionInputBytes) ||
626
+ maxActionInputBytes < 1 ||
627
+ maxActionInputBytes > MAX_ACTION_INPUT_BYTES_V1)
628
+ throw new Error('dependency_rebind_failed');
629
+ this.maxActionInputBytes = maxActionInputBytes;
630
+ this.binding = freezeBinding(binding);
631
+ this.dispatcher = Object.freeze({
632
+ dispatcherId: dispatcher.dispatcherId,
633
+ dispatcherVersion: dispatcher.dispatcherVersion,
634
+ fencingMode: dispatcher.fencingMode,
635
+ dispatch: dispatcher.dispatch.bind(dispatcher),
636
+ });
637
+ this.contextResolver = Object.freeze({
638
+ contextResolverId: contextResolver.contextResolverId,
639
+ contextResolverVersion: contextResolver.contextResolverVersion,
640
+ resolve: contextResolver.resolve.bind(contextResolver),
641
+ });
642
+ this.authorityResolver = Object.freeze({
643
+ resolverId: authorityResolver.resolverId,
644
+ resolverVersion: authorityResolver.resolverVersion,
645
+ resolve: authorityResolver.resolve.bind(authorityResolver),
646
+ });
647
+ this.assessmentResolver = Object.freeze({
648
+ assessorId: assessmentResolver.assessorId,
649
+ assessorVersion: assessmentResolver.assessorVersion,
650
+ consumeCurrent: assessmentResolver.consumeCurrent.bind(assessmentResolver),
651
+ });
652
+ }
653
+ async invoke(input) {
654
+ if (!isPlainRecord(input) ||
655
+ !hasExactKeys(input, Object.hasOwn(input, 'input')
656
+ ? ['schemaVersion', 'grantId', 'input', 'logicalTimeMs']
657
+ : ['schemaVersion', 'grantId', 'logicalTimeMs']) ||
658
+ input.schemaVersion !== 1 ||
659
+ !nonEmptyString(input.grantId))
660
+ throw new Error('action_not_permitted');
661
+ const value = freezeControlJsonObject(input.input ?? {}, this.maxActionInputBytes);
662
+ const digest = actionInputDigest(value);
663
+ const grant = this.ledger.get(input.grantId);
664
+ if (!grant)
665
+ throw new Error('grant_missing');
666
+ if (grant.actionBindingId !== this.binding.actionBindingId ||
667
+ grant.actionBindingVersion !== this.binding.actionBindingVersion ||
668
+ grant.namespace !== this.binding.namespace ||
669
+ grant.toolId !== this.binding.toolId ||
670
+ grant.operation !== this.binding.operation ||
671
+ grant.handlerDigest !== this.binding.handlerDigest ||
672
+ this.binding.dispatcherId !== this.dispatcher.dispatcherId ||
673
+ this.binding.dispatcherVersion !== this.dispatcher.dispatcherVersion ||
674
+ this.binding.contextResolverId !==
675
+ this.contextResolver.contextResolverId ||
676
+ this.binding.contextResolverVersion !==
677
+ this.contextResolver.contextResolverVersion ||
678
+ this.binding.fencingMode !== this.dispatcher.fencingMode ||
679
+ grant.inputDigest !== digest ||
680
+ 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');
686
+ if (!(await this.assessmentResolver.consumeCurrent(grant, input.logicalTimeMs)))
687
+ throw new Error('grant_assessment_mismatch');
688
+ 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);
690
+ const reservation = reserved.reservation;
691
+ try {
692
+ const context = freezeToolContext(await this.contextResolver.resolve(reserved.scope, this.binding));
693
+ if (context.tenant.tenantId !== reserved.scope.tenantId ||
694
+ context.toolId !== this.binding.toolId ||
695
+ context.runId !== reserved.scope.runId)
696
+ throw new Error('invocation_context_mismatch');
697
+ const latest = normalizeAuthorityResultV1(this.authorityResolver, reserved.scope, reserved.actionDigest, await this.authorityResolver.resolve(reserved.scope, reserved.actionDigest, input.logicalTimeMs));
698
+ 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');
701
+ }
702
+ let assessmentStillCurrent = false;
703
+ try {
704
+ assessmentStillCurrent = await this.assessmentResolver.consumeCurrent(grant, input.logicalTimeMs);
705
+ }
706
+ catch {
707
+ assessmentStillCurrent = false;
708
+ }
709
+ if (!assessmentStillCurrent) {
710
+ settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, 'failed');
711
+ throw new Error('grant_assessment_mismatch');
712
+ }
713
+ const permit = Object.freeze({
714
+ schemaVersion: 1,
715
+ grantId: reserved.grantId,
716
+ reservationId: reservation.reservationId,
717
+ dispatchAttemptId: reservation.dispatchAttemptId,
718
+ gatewayId: this.ledger.gatewayId,
719
+ scopeDigest: reserved.scopeDigest,
720
+ actionDigest: reserved.actionDigest,
721
+ idempotencyKey: reserved.idempotencyKey,
722
+ authorityGeneration: reservation.authorityGeneration,
723
+ fencingToken: reservation.fencingToken,
724
+ });
725
+ const result = await this.dispatcher.dispatch({
726
+ binding: this.binding,
727
+ input: value,
728
+ context,
729
+ permit,
730
+ });
731
+ settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId, result.ok ? 'dispatched' : 'failed');
732
+ return result;
733
+ }
734
+ 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');
738
+ throw error;
739
+ }
740
+ }
741
+ }
742
+ export async function freezeToolRegistryBinding(registry, toolId, binding) {
743
+ const found = await registry.get(toolId);
744
+ if (!found)
745
+ throw new Error('action_not_permitted');
746
+ const handlerDigest = controlDigest('handler-binding', {
747
+ definition: found.definition,
748
+ binding: { ...binding, toolId },
749
+ });
750
+ return Object.freeze({
751
+ binding: Object.freeze({ ...binding, toolId, handlerDigest }),
752
+ handler: found.handler,
753
+ });
754
+ }
755
+ //# sourceMappingURL=tools.js.map