@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.d.ts +69 -23
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +420 -229
- package/dist/tools.js.map +1 -1
- package/package.json +7 -7
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalizeControlJsonV1, digestControlJsonV1, utf8ByteLength, } from
|
|
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 ===
|
|
10
|
-
return digestControlJsonV1(
|
|
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(
|
|
14
|
+
return controlDigest("action-input", input);
|
|
15
15
|
}
|
|
16
16
|
export function scopeDigest(scope) {
|
|
17
|
-
return controlDigest(
|
|
17
|
+
return controlDigest("scope", scope);
|
|
18
18
|
}
|
|
19
19
|
export function actionDigest(grant, binding) {
|
|
20
|
-
return controlDigest(
|
|
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(
|
|
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 !==
|
|
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(
|
|
68
|
+
throw new TypeError("Invalid Action Grant");
|
|
71
69
|
const prior = this.grants.get(grant.grantId);
|
|
72
70
|
if (prior) {
|
|
73
|
-
if (controlDigest(
|
|
74
|
-
controlDigest(
|
|
75
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
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 ===
|
|
115
|
+
const grant = source.status === "reserved"
|
|
155
116
|
? {
|
|
156
117
|
...source,
|
|
157
|
-
status:
|
|
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(
|
|
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(
|
|
128
|
+
throw new Error("grant_idempotency_conflict");
|
|
168
129
|
if (previous)
|
|
169
|
-
throw new Error(
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
205
|
-
return this.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
const
|
|
213
|
-
if (!
|
|
214
|
-
|
|
215
|
-
|
|
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(
|
|
213
|
+
throw new Error("logical_time_rollback");
|
|
221
214
|
this.highWater = value;
|
|
222
215
|
}
|
|
223
216
|
}
|
|
224
|
-
/**
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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(
|
|
252
|
-
if (grant.status ===
|
|
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(
|
|
394
|
+
throw new TypeError("Invalid Action Grant snapshot");
|
|
255
395
|
}
|
|
256
396
|
else if (!isActionReservation(grant.reservation, highWater, grant)) {
|
|
257
|
-
throw new TypeError(
|
|
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 !==
|
|
468
|
+
if (authority.status !== "current" ||
|
|
279
469
|
!isActionScopeV1(authority.scope) ||
|
|
280
470
|
scopeDigest(authority.scope) !== expectedScopeDigest)
|
|
281
|
-
throw new Error(
|
|
282
|
-
if (scope.kind ===
|
|
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(
|
|
286
|
-
if (scope.kind ===
|
|
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(
|
|
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(
|
|
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(
|
|
308
|
-
if (value.status ===
|
|
497
|
+
throw new Error("authority_result_invalid");
|
|
498
|
+
if (value.status === "stale" || value.status === "unavailable") {
|
|
309
499
|
if (!hasExactKeys(value, [
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
500
|
+
"schemaVersion",
|
|
501
|
+
"status",
|
|
502
|
+
"resolverId",
|
|
503
|
+
"resolverVersion",
|
|
504
|
+
"scopeDigest",
|
|
505
|
+
"actionDigest",
|
|
316
506
|
]))
|
|
317
|
-
throw new Error(
|
|
507
|
+
throw new Error("authority_result_invalid");
|
|
318
508
|
return Object.freeze({ ...value });
|
|
319
509
|
}
|
|
320
|
-
if (value.status !==
|
|
510
|
+
if (value.status !== "current" ||
|
|
321
511
|
!hasExactKeys(value, [
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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 !==
|
|
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(
|
|
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 !==
|
|
536
|
+
if (authority.status !== "current")
|
|
347
537
|
return null;
|
|
348
|
-
return scope.kind ===
|
|
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 !==
|
|
543
|
+
if (authority.status !== "current")
|
|
354
544
|
return null;
|
|
355
|
-
return scope.kind ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
589
|
+
throw new TypeError("Canonical JSON exceeds its byte limit");
|
|
400
590
|
};
|
|
401
591
|
const active = new Set();
|
|
402
|
-
const stack = [{ kind:
|
|
592
|
+
const stack = [{ kind: "enter", value, depth: 0 }];
|
|
403
593
|
while (stack.length) {
|
|
404
594
|
const item = stack.pop();
|
|
405
|
-
if (item.kind ===
|
|
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(
|
|
601
|
+
throw new TypeError("Canonical JSON contains too many nodes");
|
|
412
602
|
if (item.depth > MAX_CONTROL_JSON_DEPTH_V1)
|
|
413
|
-
throw new TypeError(
|
|
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 ===
|
|
609
|
+
if (valueType === "string") {
|
|
420
610
|
if (item.value.length > maximumBytes)
|
|
421
|
-
throw new TypeError(
|
|
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 ===
|
|
615
|
+
if (valueType === "boolean") {
|
|
426
616
|
addCanonicalBytes(item.value ? 4 : 5);
|
|
427
617
|
continue;
|
|
428
618
|
}
|
|
429
|
-
if (valueType ===
|
|
619
|
+
if (valueType === "number") {
|
|
430
620
|
if (!Number.isFinite(item.value))
|
|
431
|
-
throw new TypeError(
|
|
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 !==
|
|
438
|
-
throw new TypeError(
|
|
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(
|
|
631
|
+
throw new TypeError("Canonical JSON cannot contain cycles");
|
|
442
632
|
active.add(objectValue);
|
|
443
|
-
stack.push({ kind:
|
|
633
|
+
stack.push({ kind: "exit", value: objectValue });
|
|
444
634
|
const keys = Reflect.ownKeys(objectValue);
|
|
445
|
-
if (keys.some((key) => typeof key ===
|
|
446
|
-
throw new TypeError(
|
|
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(
|
|
450
|
-
if (keys.length !== objectValue.length + 1 || !keys.includes(
|
|
451
|
-
throw new TypeError(
|
|
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 || !(
|
|
456
|
-
throw new TypeError(
|
|
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:
|
|
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(
|
|
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(
|
|
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 || !(
|
|
476
|
-
throw new TypeError(
|
|
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:
|
|
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(
|
|
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,
|
|
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 ===
|
|
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 !==
|
|
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) =>
|
|
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) =>
|
|
707
|
+
Object.values(Object.getOwnPropertyDescriptors(value)).every((descriptor) => "value" in descriptor));
|
|
518
708
|
}
|
|
519
709
|
function nonEmptyString(value) {
|
|
520
|
-
return typeof value ===
|
|
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 ===
|
|
716
|
+
return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
|
|
527
717
|
}
|
|
528
718
|
function isGrantStatus(value) {
|
|
529
|
-
return (value ===
|
|
530
|
-
value ===
|
|
531
|
-
value ===
|
|
532
|
-
value ===
|
|
533
|
-
value ===
|
|
534
|
-
value ===
|
|
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 ===
|
|
736
|
+
if (value.kind === "standalone")
|
|
547
737
|
return (hasExactKeys(value, [
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
738
|
+
"schemaVersion",
|
|
739
|
+
"kind",
|
|
740
|
+
"tenantId",
|
|
741
|
+
"runId",
|
|
742
|
+
"agentId",
|
|
743
|
+
"organizationId",
|
|
744
|
+
"workspaceId",
|
|
745
|
+
"policyId",
|
|
746
|
+
"policyVersion",
|
|
557
747
|
]) &&
|
|
558
|
-
(typeof value.organizationId ===
|
|
748
|
+
(typeof value.organizationId === "string" ||
|
|
559
749
|
value.organizationId === null) &&
|
|
560
|
-
(typeof value.workspaceId ===
|
|
561
|
-
return (value.kind ===
|
|
750
|
+
(typeof value.workspaceId === "string" || value.workspaceId === null));
|
|
751
|
+
return (value.kind === "coordinated" &&
|
|
562
752
|
hasExactKeys(value, [
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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 ===
|
|
601
|
-
typeof value.objectiveTerminal ===
|
|
602
|
-
typeof value.workTerminal ===
|
|
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(
|
|
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(
|
|
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,
|
|
656
|
-
? [
|
|
657
|
-
: [
|
|
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(
|
|
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.
|
|
853
|
+
const grant = await this.ledger.loadGrant(input.grantId);
|
|
664
854
|
if (!grant)
|
|
665
|
-
throw new Error(
|
|
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(
|
|
682
|
-
if (grant.scope.kind ===
|
|
683
|
-
(this.binding.fencingMode !==
|
|
684
|
-
this.dispatcher.fencingMode !==
|
|
685
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
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,
|
|
700
|
-
throw new Error(
|
|
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,
|
|
711
|
-
throw new Error(
|
|
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 ?
|
|
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.
|
|
736
|
-
if (current?.status ===
|
|
737
|
-
settleGrantForGateway(this.ledger, reserved.grantId, reservation.reservationId, reservation.dispatchAttemptId,
|
|
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(
|
|
746
|
-
const handlerDigest = controlDigest(
|
|
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
|
});
|