@orchestrator-claude/definitions 3.43.1 → 3.44.0
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/hooks/gate-guardian.mjs +174 -54
- package/hooks/gate-guardian.ts +160 -56
- package/hooks/lib/api-client.ts +124 -21
- package/hooks/lib/gate-logic.ts +6 -5
- package/manifest.json +3 -3
- package/package.json +1 -1
package/hooks/gate-guardian.mjs
CHANGED
|
@@ -213,18 +213,74 @@ async function getGateCellMode(typeId, variant, gateKey, token) {
|
|
|
213
213
|
return null;
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
+
async function apiGetWithStatus(path, token) {
|
|
217
|
+
try {
|
|
218
|
+
const resp = await fetch(`${API_URL}${path}`, {
|
|
219
|
+
headers: {
|
|
220
|
+
Authorization: `Bearer ${token}`,
|
|
221
|
+
"X-Project-ID": PROJECT_ID
|
|
222
|
+
},
|
|
223
|
+
signal: AbortSignal.timeout(5e3)
|
|
224
|
+
});
|
|
225
|
+
return { kind: "response", resp };
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const name = typeof err === "object" && err !== null && "name" in err ? err.name : "";
|
|
228
|
+
const timedOut = name === "TimeoutError" || name === "AbortError";
|
|
229
|
+
return { kind: "error", reason: timedOut ? "timeout" : "network" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function iterationDetail(raw) {
|
|
233
|
+
const json = JSON.stringify(raw);
|
|
234
|
+
return json === void 0 ? void 0 : json.slice(0, 40);
|
|
235
|
+
}
|
|
236
|
+
function parseIteration(raw) {
|
|
237
|
+
if (!/^[0-9]+$/.test(raw)) return null;
|
|
238
|
+
const value = Number(raw);
|
|
239
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
240
|
+
}
|
|
241
|
+
function classifyIteration(verdict, rawIteration) {
|
|
242
|
+
if (typeof rawIteration !== "string") {
|
|
243
|
+
const detail = iterationDetail(rawIteration);
|
|
244
|
+
return detail === void 0 ? { kind: "malformed", reason: "iteration_invalid" } : { kind: "malformed", reason: "iteration_invalid", detail };
|
|
245
|
+
}
|
|
246
|
+
const iteration = parseIteration(rawIteration);
|
|
247
|
+
if (iteration === null) {
|
|
248
|
+
return { kind: "iteration_rejected", verdict, detail: iterationDetail(rawIteration) ?? "" };
|
|
249
|
+
}
|
|
250
|
+
return { kind: "ok", verdict, iteration };
|
|
251
|
+
}
|
|
252
|
+
function classifyAdversarialBody(body) {
|
|
253
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
254
|
+
return { kind: "malformed", reason: "body_not_object" };
|
|
255
|
+
}
|
|
256
|
+
const metadata = "metadata" in body ? body.metadata : void 0;
|
|
257
|
+
const customMetadata = typeof metadata === "object" && metadata !== null && "customMetadata" in metadata ? metadata.customMetadata : void 0;
|
|
258
|
+
if (typeof customMetadata !== "object" || customMetadata === null) {
|
|
259
|
+
return { kind: "malformed", reason: "custom_metadata_missing" };
|
|
260
|
+
}
|
|
261
|
+
const verdict = "verdict" in customMetadata ? customMetadata.verdict : void 0;
|
|
262
|
+
if (typeof verdict !== "string") return { kind: "malformed", reason: "verdict_invalid" };
|
|
263
|
+
const rawIteration = "iteration" in customMetadata ? customMetadata.iteration : void 0;
|
|
264
|
+
return classifyIteration(verdict, rawIteration);
|
|
265
|
+
}
|
|
216
266
|
async function getAdversarialVerdict(workflowId, token) {
|
|
217
267
|
try {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
if (!
|
|
225
|
-
|
|
268
|
+
const got = await apiGetWithStatus(
|
|
269
|
+
`/api/v1/artifact-content/${workflowId}/adversarial_review`,
|
|
270
|
+
token
|
|
271
|
+
);
|
|
272
|
+
if (got.kind === "error") return got;
|
|
273
|
+
if (got.resp.status === 404) return { kind: "absent" };
|
|
274
|
+
if (!got.resp.ok) return { kind: "error", reason: "http", status: got.resp.status };
|
|
275
|
+
let body;
|
|
276
|
+
try {
|
|
277
|
+
body = await got.resp.json();
|
|
278
|
+
} catch {
|
|
279
|
+
return { kind: "malformed", reason: "body_not_json" };
|
|
280
|
+
}
|
|
281
|
+
return classifyAdversarialBody(body);
|
|
226
282
|
} catch {
|
|
227
|
-
return
|
|
283
|
+
return { kind: "error", reason: "unexpected" };
|
|
228
284
|
}
|
|
229
285
|
}
|
|
230
286
|
async function readStdin() {
|
|
@@ -276,8 +332,8 @@ function deriveGateStrategy(ctx) {
|
|
|
276
332
|
};
|
|
277
333
|
}
|
|
278
334
|
return {
|
|
279
|
-
strategy: "
|
|
280
|
-
reason: `Phase '${ctx.targetPhase}' is low-risk \u2014
|
|
335
|
+
strategy: "ALLOW_LOW_RISK",
|
|
336
|
+
reason: `Phase '${ctx.targetPhase}' is low-risk \u2014 allowed without an approval check; no approval observed by this hook.`
|
|
281
337
|
};
|
|
282
338
|
}
|
|
283
339
|
|
|
@@ -295,6 +351,107 @@ var defaultDeps = {
|
|
|
295
351
|
output: outputPreToolUse,
|
|
296
352
|
log
|
|
297
353
|
};
|
|
354
|
+
var RESULT_KINDS = /* @__PURE__ */ new Set([
|
|
355
|
+
"ok",
|
|
356
|
+
"absent",
|
|
357
|
+
"malformed",
|
|
358
|
+
"iteration_rejected",
|
|
359
|
+
"error"
|
|
360
|
+
]);
|
|
361
|
+
function normalizeVerdictResult(raw) {
|
|
362
|
+
const known = typeof raw === "object" && raw !== null && "kind" in raw && RESULT_KINDS.has(String(raw.kind));
|
|
363
|
+
return known ? raw : { kind: "error", reason: "unexpected" };
|
|
364
|
+
}
|
|
365
|
+
function decideOkVerdict(result, workflowId, targetPhase, output, log2) {
|
|
366
|
+
if (result.iteration >= PME_HARD_CAP) {
|
|
367
|
+
log2(
|
|
368
|
+
HOOK,
|
|
369
|
+
`PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(result.iteration)}) \u2014 ALLOW wf=${workflowId}`
|
|
370
|
+
);
|
|
371
|
+
output({
|
|
372
|
+
decision: "allow",
|
|
373
|
+
context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(result.iteration)}/${String(PME_HARD_CAP)}), escalating.`
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (result.verdict === "PASS") {
|
|
378
|
+
log2(HOOK, `PME Step 5.5: adversarial verdict=PASS \u2014 ALLOW wf=${workflowId}`);
|
|
379
|
+
output({
|
|
380
|
+
decision: "allow",
|
|
381
|
+
context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
log2(
|
|
386
|
+
HOOK,
|
|
387
|
+
`PME Step 5.5: DENY verdict=${result.verdict} iteration=${String(result.iteration)} wf=${workflowId}`
|
|
388
|
+
);
|
|
389
|
+
output({
|
|
390
|
+
decision: "deny",
|
|
391
|
+
reason: `Gate Guardian: adversarial_review required \u2014 verdict=${result.verdict}, iteration=${String(result.iteration)}. Resolve adversarial review before advancing.`
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function decideRejectedIteration(result, workflowId, targetPhase, output, log2) {
|
|
395
|
+
const { verdict, detail } = result;
|
|
396
|
+
if (verdict === "PASS") {
|
|
397
|
+
log2(
|
|
398
|
+
HOOK,
|
|
399
|
+
`PME Step 5.5: adversarial verdict=PASS with iteration_invalid (${detail}) \u2014 ALLOW wf=${workflowId}`
|
|
400
|
+
);
|
|
401
|
+
output({
|
|
402
|
+
decision: "allow",
|
|
403
|
+
context: `[GATE] PME adversarial gate: verdict=PASS, iteration_invalid (${detail}), hard cap not evaluated, proceeding to '${targetPhase}'.`
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
log2(HOOK, `PME Step 5.5: DENY verdict=${verdict} iteration_invalid (${detail}) wf=${workflowId}`);
|
|
408
|
+
output({
|
|
409
|
+
decision: "deny",
|
|
410
|
+
reason: `Gate Guardian: adversarial_review iteration invalid (${detail}) with verdict=${verdict} \u2014 the hard cap cannot apply. Rewrite the adversarial_review artifact with customMetadata.iteration as a non-negative decimal integer (e.g. '1') and retry.`
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function decideAdversarialResult(raw, workflowId, targetPhase, output, log2) {
|
|
414
|
+
const result = normalizeVerdictResult(raw);
|
|
415
|
+
switch (result.kind) {
|
|
416
|
+
case "ok":
|
|
417
|
+
decideOkVerdict(result, workflowId, targetPhase, output, log2);
|
|
418
|
+
return;
|
|
419
|
+
case "iteration_rejected":
|
|
420
|
+
decideRejectedIteration(result, workflowId, targetPhase, output, log2);
|
|
421
|
+
return;
|
|
422
|
+
case "absent":
|
|
423
|
+
log2(HOOK, `PME Step 5.5: no adversarial artifact found \u2014 fail-open ALLOW wf=${workflowId}`);
|
|
424
|
+
output({
|
|
425
|
+
decision: "allow",
|
|
426
|
+
context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
case "malformed": {
|
|
430
|
+
const detail = result.detail === void 0 ? "" : ` detail=${result.detail}`;
|
|
431
|
+
log2(
|
|
432
|
+
HOOK,
|
|
433
|
+
`PME Step 5.5: adversarial artifact present but malformed (${result.reason}${detail}) \u2014 fail-open ALLOW wf=${workflowId}`
|
|
434
|
+
);
|
|
435
|
+
output({
|
|
436
|
+
decision: "allow",
|
|
437
|
+
context: `[GATE] PME adversarial gate: artifact malformed (${result.reason}), fail-open ALLOW for '${targetPhase}'.`
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
case "error": {
|
|
442
|
+
const status = result.status === void 0 ? "" : ` status=${String(result.status)}`;
|
|
443
|
+
log2(
|
|
444
|
+
HOOK,
|
|
445
|
+
`PME Step 5.5: adversarial artifact unreadable (${result.reason}${status}) \u2014 fail-open ALLOW wf=${workflowId}`
|
|
446
|
+
);
|
|
447
|
+
output({
|
|
448
|
+
decision: "allow",
|
|
449
|
+
context: `[GATE] PME adversarial gate: artifact could not be read (${result.reason}), fail-open ALLOW for '${targetPhase}'.`
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
298
455
|
async function mainWithDeps(deps) {
|
|
299
456
|
const {
|
|
300
457
|
readStdin: readStdin2,
|
|
@@ -318,7 +475,7 @@ async function mainWithDeps(deps) {
|
|
|
318
475
|
if (mode === "quick") {
|
|
319
476
|
output({
|
|
320
477
|
decision: "allow",
|
|
321
|
-
context: `[GATE]
|
|
478
|
+
context: `[GATE] ALLOW_QUICK_MODE: quick mode skips gate evaluation for '${targetPhase}'; no approval observed by this hook.`
|
|
322
479
|
});
|
|
323
480
|
log2(HOOK, `ALLOW quick mode phase=${targetPhase}`);
|
|
324
481
|
return;
|
|
@@ -326,7 +483,7 @@ async function mainWithDeps(deps) {
|
|
|
326
483
|
if (mode === "interactive") {
|
|
327
484
|
output({
|
|
328
485
|
decision: "allow",
|
|
329
|
-
context: `[GATE]
|
|
486
|
+
context: `[GATE] ALLOW_INTERACTIVE_MODE: interactive mode skips artifact gate evaluation for '${targetPhase}'; no approval observed by this hook.`
|
|
330
487
|
});
|
|
331
488
|
log2(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
332
489
|
return;
|
|
@@ -361,45 +518,8 @@ async function mainWithDeps(deps) {
|
|
|
361
518
|
token
|
|
362
519
|
);
|
|
363
520
|
if (gateMode === "REQ") {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
log2(
|
|
367
|
-
HOOK,
|
|
368
|
-
`PME Step 5.5: no adversarial artifact found \u2014 fail-open ALLOW wf=${workflowId}`
|
|
369
|
-
);
|
|
370
|
-
output({
|
|
371
|
-
decision: "allow",
|
|
372
|
-
context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`
|
|
373
|
-
});
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
if (verdict.iteration >= PME_HARD_CAP) {
|
|
377
|
-
log2(
|
|
378
|
-
HOOK,
|
|
379
|
-
`PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(verdict.iteration)}) \u2014 ALLOW wf=${workflowId}`
|
|
380
|
-
);
|
|
381
|
-
output({
|
|
382
|
-
decision: "allow",
|
|
383
|
-
context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(verdict.iteration)}/${String(PME_HARD_CAP)}), escalating.`
|
|
384
|
-
});
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if (verdict.verdict === "PASS") {
|
|
388
|
-
log2(HOOK, `PME Step 5.5: adversarial verdict=PASS \u2014 ALLOW wf=${workflowId}`);
|
|
389
|
-
output({
|
|
390
|
-
decision: "allow",
|
|
391
|
-
context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`
|
|
392
|
-
});
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
log2(
|
|
396
|
-
HOOK,
|
|
397
|
-
`PME Step 5.5: DENY verdict=${verdict.verdict} iteration=${String(verdict.iteration)} wf=${workflowId}`
|
|
398
|
-
);
|
|
399
|
-
output({
|
|
400
|
-
decision: "deny",
|
|
401
|
-
reason: `Gate Guardian: adversarial_review required \u2014 verdict=${verdict.verdict}, iteration=${String(verdict.iteration)}. Resolve adversarial review before advancing.`
|
|
402
|
-
});
|
|
521
|
+
const result = await getAdversarialVerdict2(workflowId, token);
|
|
522
|
+
decideAdversarialResult(result, workflowId, targetPhase, output, log2);
|
|
403
523
|
return;
|
|
404
524
|
}
|
|
405
525
|
log2(
|
|
@@ -467,10 +587,10 @@ async function mainWithDeps(deps) {
|
|
|
467
587
|
JSON.stringify({ strategy: decision.strategy, targetPhase, riskLevel, reason: decision.reason })
|
|
468
588
|
);
|
|
469
589
|
switch (decision.strategy) {
|
|
470
|
-
case "
|
|
590
|
+
case "ALLOW_LOW_RISK":
|
|
471
591
|
output({
|
|
472
592
|
decision: "allow",
|
|
473
|
-
context: `[GATE]
|
|
593
|
+
context: `[GATE] ALLOW_LOW_RISK: ${decision.reason}`
|
|
474
594
|
});
|
|
475
595
|
break;
|
|
476
596
|
case "ASK_HUMAN":
|
package/hooks/gate-guardian.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 6. classifyRisk(targetPhase) → riskLevel
|
|
14
14
|
* 7. buildGateContext() + deriveGateStrategy() → GateDecision
|
|
15
15
|
* 8. switch strategy:
|
|
16
|
-
*
|
|
16
|
+
* ALLOW_LOW_RISK → allow + context (no approval observed by this hook)
|
|
17
17
|
* ASK_HUMAN → deny + approval instructions
|
|
18
18
|
* BLOCK_AND_REQUIRE → deny + requiredAction
|
|
19
19
|
* 9. log(HOOK, JSON summary)
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
getGateCellMode,
|
|
31
31
|
getAdversarialVerdict,
|
|
32
32
|
outputPreToolUse,
|
|
33
|
+
type AdversarialVerdictResult,
|
|
33
34
|
type PendingAction,
|
|
34
35
|
} from './lib/api-client.js';
|
|
35
36
|
import { classifyRisk, deriveGateStrategy, type GateContext } from './lib/gate-logic.js';
|
|
@@ -40,7 +41,9 @@ const HOOK = 'GATE-GUARDIAN';
|
|
|
40
41
|
* Maximum adversarial review iterations before the hard cap triggers.
|
|
41
42
|
* When iteration >= PME_HARD_CAP the gate ALLOWS (escalation path) and logs
|
|
42
43
|
* a BLOCKED-escalation warning. This prevents infinite review loops.
|
|
43
|
-
* RFC-031 §4.4 documents this mechanic.
|
|
44
|
+
* RFC-031 §4.4 documents this mechanic. The cap only applies to a well-formed
|
|
45
|
+
* `iteration` (kind 'ok'); an `iteration` the helper rejected has no count to
|
|
46
|
+
* compare, so a non-PASS verdict with it is denied instead (TD-375).
|
|
44
47
|
*/
|
|
45
48
|
const PME_HARD_CAP = 3;
|
|
46
49
|
|
|
@@ -65,11 +68,8 @@ export interface HookDeps {
|
|
|
65
68
|
gateKey: string,
|
|
66
69
|
token: string
|
|
67
70
|
) => Promise<'REQ' | 'OPT' | 'SKIP' | null>;
|
|
68
|
-
/** PME Step 5.5: retrieve adversarial review verdict
|
|
69
|
-
getAdversarialVerdict: (
|
|
70
|
-
workflowId: string,
|
|
71
|
-
token: string
|
|
72
|
-
) => Promise<{ verdict: string; iteration: number } | null>;
|
|
71
|
+
/** PME Step 5.5: retrieve the classified adversarial review verdict (TD-374; never rejects) */
|
|
72
|
+
getAdversarialVerdict: (workflowId: string, token: string) => Promise<AdversarialVerdictResult>;
|
|
73
73
|
output: (opts: { decision: 'allow' | 'deny'; reason?: string; context?: string }) => void;
|
|
74
74
|
log: (prefix: string, msg: string) => void;
|
|
75
75
|
}
|
|
@@ -86,6 +86,148 @@ const defaultDeps: HookDeps = {
|
|
|
86
86
|
log,
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
// ─────────────────────────────────────────────────────────────
|
|
90
|
+
// PME Step 5.5: one decision per AdversarialVerdictResult kind
|
|
91
|
+
// ─────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
type Output = HookDeps['output'];
|
|
94
|
+
type Log = HookDeps['log'];
|
|
95
|
+
|
|
96
|
+
const RESULT_KINDS: ReadonlySet<string> = new Set([
|
|
97
|
+
'ok',
|
|
98
|
+
'absent',
|
|
99
|
+
'malformed',
|
|
100
|
+
'iteration_rejected',
|
|
101
|
+
'error',
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
/** null / undefined / unknown kind (a stale dep or a bug) is an unexpected error, never a throw. */
|
|
105
|
+
function normalizeVerdictResult(raw: unknown): AdversarialVerdictResult {
|
|
106
|
+
const known =
|
|
107
|
+
typeof raw === 'object' && raw !== null && 'kind' in raw && RESULT_KINDS.has(String(raw.kind));
|
|
108
|
+
return known ? (raw as AdversarialVerdictResult) : { kind: 'error', reason: 'unexpected' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Applies the gate for an `ok` verdict: hard cap -> PASS -> DENY (unchanged texts). */
|
|
112
|
+
function decideOkVerdict(
|
|
113
|
+
result: Extract<AdversarialVerdictResult, { kind: 'ok' }>,
|
|
114
|
+
workflowId: string,
|
|
115
|
+
targetPhase: string,
|
|
116
|
+
output: Output,
|
|
117
|
+
log: Log
|
|
118
|
+
): void {
|
|
119
|
+
if (result.iteration >= PME_HARD_CAP) {
|
|
120
|
+
// Hard cap reached — ALLOW with escalation log (prevents infinite loops)
|
|
121
|
+
log(
|
|
122
|
+
HOOK,
|
|
123
|
+
`PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(result.iteration)}) — ALLOW wf=${workflowId}`
|
|
124
|
+
);
|
|
125
|
+
output({
|
|
126
|
+
decision: 'allow',
|
|
127
|
+
context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(result.iteration)}/${String(PME_HARD_CAP)}), escalating.`,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (result.verdict === 'PASS') {
|
|
132
|
+
log(HOOK, `PME Step 5.5: adversarial verdict=PASS — ALLOW wf=${workflowId}`);
|
|
133
|
+
output({
|
|
134
|
+
decision: 'allow',
|
|
135
|
+
context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Verdict is NEEDS_REVISION (or unknown) and below hard cap — DENY
|
|
140
|
+
log(
|
|
141
|
+
HOOK,
|
|
142
|
+
`PME Step 5.5: DENY verdict=${result.verdict} iteration=${String(result.iteration)} wf=${workflowId}`
|
|
143
|
+
);
|
|
144
|
+
output({
|
|
145
|
+
decision: 'deny',
|
|
146
|
+
reason: `Gate Guardian: adversarial_review required — verdict=${result.verdict}, iteration=${String(result.iteration)}. Resolve adversarial review before advancing.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* `iteration` was a string that is not a canonical decimal integer (TD-375).
|
|
152
|
+
* PASS needs no hard cap, so it proceeds; any other verdict is denied with the
|
|
153
|
+
* way out spelled in the reason (the writer must fix the artifact).
|
|
154
|
+
*/
|
|
155
|
+
function decideRejectedIteration(
|
|
156
|
+
result: Extract<AdversarialVerdictResult, { kind: 'iteration_rejected' }>,
|
|
157
|
+
workflowId: string,
|
|
158
|
+
targetPhase: string,
|
|
159
|
+
output: Output,
|
|
160
|
+
log: Log
|
|
161
|
+
): void {
|
|
162
|
+
const { verdict, detail } = result;
|
|
163
|
+
if (verdict === 'PASS') {
|
|
164
|
+
log(
|
|
165
|
+
HOOK,
|
|
166
|
+
`PME Step 5.5: adversarial verdict=PASS with iteration_invalid (${detail}) — ALLOW wf=${workflowId}`
|
|
167
|
+
);
|
|
168
|
+
output({
|
|
169
|
+
decision: 'allow',
|
|
170
|
+
context: `[GATE] PME adversarial gate: verdict=PASS, iteration_invalid (${detail}), hard cap not evaluated, proceeding to '${targetPhase}'.`,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
log(HOOK, `PME Step 5.5: DENY verdict=${verdict} iteration_invalid (${detail}) wf=${workflowId}`);
|
|
175
|
+
output({
|
|
176
|
+
decision: 'deny',
|
|
177
|
+
reason: `Gate Guardian: adversarial_review iteration invalid (${detail}) with verdict=${verdict} — the hard cap cannot apply. Rewrite the adversarial_review artifact with customMetadata.iteration as a non-negative decimal integer (e.g. '1') and retry.`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Maps one classified adversarial result to ALLOW/DENY. Fail-open except a rejected iteration. */
|
|
182
|
+
function decideAdversarialResult(
|
|
183
|
+
raw: unknown,
|
|
184
|
+
workflowId: string,
|
|
185
|
+
targetPhase: string,
|
|
186
|
+
output: Output,
|
|
187
|
+
log: Log
|
|
188
|
+
): void {
|
|
189
|
+
const result = normalizeVerdictResult(raw);
|
|
190
|
+
switch (result.kind) {
|
|
191
|
+
case 'ok':
|
|
192
|
+
decideOkVerdict(result, workflowId, targetPhase, output, log);
|
|
193
|
+
return;
|
|
194
|
+
case 'iteration_rejected':
|
|
195
|
+
decideRejectedIteration(result, workflowId, targetPhase, output, log);
|
|
196
|
+
return;
|
|
197
|
+
case 'absent':
|
|
198
|
+
log(HOOK, `PME Step 5.5: no adversarial artifact found — fail-open ALLOW wf=${workflowId}`);
|
|
199
|
+
output({
|
|
200
|
+
decision: 'allow',
|
|
201
|
+
context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
case 'malformed': {
|
|
205
|
+
const detail = result.detail === undefined ? '' : ` detail=${result.detail}`;
|
|
206
|
+
log(
|
|
207
|
+
HOOK,
|
|
208
|
+
`PME Step 5.5: adversarial artifact present but malformed (${result.reason}${detail}) — fail-open ALLOW wf=${workflowId}`
|
|
209
|
+
);
|
|
210
|
+
output({
|
|
211
|
+
decision: 'allow',
|
|
212
|
+
context: `[GATE] PME adversarial gate: artifact malformed (${result.reason}), fail-open ALLOW for '${targetPhase}'.`,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
case 'error': {
|
|
217
|
+
const status = result.status === undefined ? '' : ` status=${String(result.status)}`;
|
|
218
|
+
log(
|
|
219
|
+
HOOK,
|
|
220
|
+
`PME Step 5.5: adversarial artifact unreadable (${result.reason}${status}) — fail-open ALLOW wf=${workflowId}`
|
|
221
|
+
);
|
|
222
|
+
output({
|
|
223
|
+
decision: 'allow',
|
|
224
|
+
context: `[GATE] PME adversarial gate: artifact could not be read (${result.reason}), fail-open ALLOW for '${targetPhase}'.`,
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
89
231
|
// ─────────────────────────────────────────────────────────────
|
|
90
232
|
// Core logic (exported for testing)
|
|
91
233
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -119,7 +261,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
119
261
|
if (mode === 'quick') {
|
|
120
262
|
output({
|
|
121
263
|
decision: 'allow',
|
|
122
|
-
context: `[GATE]
|
|
264
|
+
context: `[GATE] ALLOW_QUICK_MODE: quick mode skips gate evaluation for '${targetPhase}'; no approval observed by this hook.`,
|
|
123
265
|
});
|
|
124
266
|
log(HOOK, `ALLOW quick mode phase=${targetPhase}`);
|
|
125
267
|
return;
|
|
@@ -128,7 +270,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
128
270
|
if (mode === 'interactive') {
|
|
129
271
|
output({
|
|
130
272
|
decision: 'allow',
|
|
131
|
-
context: `[GATE]
|
|
273
|
+
context: `[GATE] ALLOW_INTERACTIVE_MODE: interactive mode skips artifact gate evaluation for '${targetPhase}'; no approval observed by this hook.`,
|
|
132
274
|
});
|
|
133
275
|
log(HOOK, `ALLOW interactive mode phase=${targetPhase}`);
|
|
134
276
|
return;
|
|
@@ -164,7 +306,10 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
164
306
|
// a) workflow type is 'project_management_enterprise'
|
|
165
307
|
// b) the variant's 'adversarial' gate cell is 'REQ'
|
|
166
308
|
// c) adversarial review artifact exists and verdict is not PASS
|
|
167
|
-
//
|
|
309
|
+
// Absent / malformed / unreadable artifacts are fail-open (ALLOW) so hooks never
|
|
310
|
+
// block on downtime; each is logged under its own label (TD-374). The one
|
|
311
|
+
// exception is a non-PASS verdict whose `iteration` is not a canonical decimal
|
|
312
|
+
// integer: the hard cap cannot be evaluated, so it is denied (TD-375).
|
|
168
313
|
{
|
|
169
314
|
const typeVariant = await getWorkflowTypeAndVariant(workflowId, token);
|
|
170
315
|
if (typeVariant?.type === 'project_management_enterprise') {
|
|
@@ -175,49 +320,8 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
175
320
|
token
|
|
176
321
|
);
|
|
177
322
|
if (gateMode === 'REQ') {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
// No artifact yet — fail-open: ALLOW
|
|
181
|
-
log(
|
|
182
|
-
HOOK,
|
|
183
|
-
`PME Step 5.5: no adversarial artifact found — fail-open ALLOW wf=${workflowId}`
|
|
184
|
-
);
|
|
185
|
-
output({
|
|
186
|
-
decision: 'allow',
|
|
187
|
-
context: `[GATE] PME adversarial gate: no artifact found, fail-open ALLOW for '${targetPhase}'.`,
|
|
188
|
-
});
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (verdict.iteration >= PME_HARD_CAP) {
|
|
192
|
-
// Hard cap reached — ALLOW with escalation log (prevents infinite loops)
|
|
193
|
-
log(
|
|
194
|
-
HOOK,
|
|
195
|
-
`PME Step 5.5: BLOCKED escalation at hard cap (iteration=${String(verdict.iteration)}) — ALLOW wf=${workflowId}`
|
|
196
|
-
);
|
|
197
|
-
output({
|
|
198
|
-
decision: 'allow',
|
|
199
|
-
context: `[GATE] PME adversarial gate: hard cap reached (iteration=${String(verdict.iteration)}/${String(PME_HARD_CAP)}), escalating.`,
|
|
200
|
-
});
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (verdict.verdict === 'PASS') {
|
|
204
|
-
// Adversarial review passed — ALLOW
|
|
205
|
-
log(HOOK, `PME Step 5.5: adversarial verdict=PASS — ALLOW wf=${workflowId}`);
|
|
206
|
-
output({
|
|
207
|
-
decision: 'allow',
|
|
208
|
-
context: `[GATE] PME adversarial gate: verdict=PASS, proceeding to '${targetPhase}'.`,
|
|
209
|
-
});
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
// Verdict is NEEDS_REVISION (or unknown) and below hard cap — DENY
|
|
213
|
-
log(
|
|
214
|
-
HOOK,
|
|
215
|
-
`PME Step 5.5: DENY verdict=${verdict.verdict} iteration=${String(verdict.iteration)} wf=${workflowId}`
|
|
216
|
-
);
|
|
217
|
-
output({
|
|
218
|
-
decision: 'deny',
|
|
219
|
-
reason: `Gate Guardian: adversarial_review required — verdict=${verdict.verdict}, iteration=${String(verdict.iteration)}. Resolve adversarial review before advancing.`,
|
|
220
|
-
});
|
|
323
|
+
const result = await getAdversarialVerdict(workflowId, token);
|
|
324
|
+
decideAdversarialResult(result, workflowId, targetPhase, output, log);
|
|
221
325
|
return;
|
|
222
326
|
}
|
|
223
327
|
// gateMode is OPT, SKIP, or null — fall through to Step 5
|
|
@@ -236,7 +340,7 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
236
340
|
|
|
237
341
|
// ── Step 5: IMPLEMENT short-circuit (RF-008) ─────────────────────────────
|
|
238
342
|
// If targetPhase contains "implement", check pending-action BEFORE classifyRisk.
|
|
239
|
-
// This prevents
|
|
343
|
+
// This prevents the low-risk allow path from being applied to implement phases.
|
|
240
344
|
//
|
|
241
345
|
// Phase 4: Only 'implement' phases have the approval path (via pending-action).
|
|
242
346
|
// Other HIGH-risk phases (deploy, migrate, delete) will always ASK_HUMAN → DENY.
|
|
@@ -303,10 +407,10 @@ export async function mainWithDeps(deps: HookDeps): Promise<void> {
|
|
|
303
407
|
);
|
|
304
408
|
|
|
305
409
|
switch (decision.strategy) {
|
|
306
|
-
case '
|
|
410
|
+
case 'ALLOW_LOW_RISK':
|
|
307
411
|
output({
|
|
308
412
|
decision: 'allow',
|
|
309
|
-
context: `[GATE]
|
|
413
|
+
context: `[GATE] ALLOW_LOW_RISK: ${decision.reason}`,
|
|
310
414
|
});
|
|
311
415
|
break;
|
|
312
416
|
|
package/hooks/lib/api-client.ts
CHANGED
|
@@ -1088,35 +1088,138 @@ export async function getGateCellMode(
|
|
|
1088
1088
|
}
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* Outcome of `GET /api/v1/artifact-content/:wf/adversarial_review`, classified
|
|
1093
|
+
* so the caller can tell WHY there is no usable verdict (TD-374) and decide.
|
|
1094
|
+
*
|
|
1095
|
+
* - `ok`: verdict string + `iteration` parsed from a canonical decimal string.
|
|
1096
|
+
* - `absent`: HTTP 404 (no artifact yet).
|
|
1097
|
+
* - `malformed`: HTTP 200 whose body carries no decidable verdict. `iteration_invalid`
|
|
1098
|
+
* here means `iteration` is absent or not a string.
|
|
1099
|
+
* - `iteration_rejected`: verdict is a valid string but `iteration` is a string that
|
|
1100
|
+
* fails the grammar (TD-375). Distinct kind on purpose: the caller must decide, a
|
|
1101
|
+
* `malformed` branch handled as fail-open would reopen the hole in silence.
|
|
1102
|
+
* - `error`: no usable response (HTTP != 200/404, timeout, network, unexpected).
|
|
1103
|
+
*/
|
|
1104
|
+
export type AdversarialVerdictResult =
|
|
1105
|
+
| { kind: 'ok'; verdict: string; iteration: number }
|
|
1106
|
+
| { kind: 'absent' }
|
|
1107
|
+
| {
|
|
1108
|
+
kind: 'malformed';
|
|
1109
|
+
reason:
|
|
1110
|
+
| 'body_not_json'
|
|
1111
|
+
| 'body_not_object'
|
|
1112
|
+
| 'custom_metadata_missing'
|
|
1113
|
+
| 'verdict_invalid'
|
|
1114
|
+
| 'iteration_invalid';
|
|
1115
|
+
detail?: string;
|
|
1116
|
+
}
|
|
1117
|
+
| { kind: 'iteration_rejected'; verdict: string; detail: string }
|
|
1118
|
+
| { kind: 'error'; reason: 'http' | 'timeout' | 'network' | 'unexpected'; status?: number };
|
|
1119
|
+
|
|
1120
|
+
type StatusFetch =
|
|
1121
|
+
| { readonly kind: 'response'; readonly resp: Response }
|
|
1122
|
+
| { readonly kind: 'error'; readonly reason: 'timeout' | 'network' };
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Same request as `apiGet` (URL, headers, 5s timeout) but keeps the HTTP
|
|
1126
|
+
* status and the failure cause that `apiGet` collapses into `null`.
|
|
1127
|
+
* `apiGet` itself is intentionally left untouched.
|
|
1128
|
+
*/
|
|
1129
|
+
async function apiGetWithStatus(path: string, token: string): Promise<StatusFetch> {
|
|
1130
|
+
try {
|
|
1131
|
+
const resp = await fetch(`${API_URL}${path}`, {
|
|
1132
|
+
headers: {
|
|
1133
|
+
Authorization: `Bearer ${token}`,
|
|
1134
|
+
'X-Project-ID': PROJECT_ID,
|
|
1135
|
+
},
|
|
1136
|
+
signal: AbortSignal.timeout(5000),
|
|
1137
|
+
});
|
|
1138
|
+
return { kind: 'response', resp };
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
const name: unknown = typeof err === 'object' && err !== null && 'name' in err ? err.name : '';
|
|
1141
|
+
const timedOut = name === 'TimeoutError' || name === 'AbortError';
|
|
1142
|
+
return { kind: 'error', reason: timedOut ? 'timeout' : 'network' };
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/** `JSON.stringify(raw)` capped at 40 chars; undefined when raw has no JSON form. */
|
|
1147
|
+
function iterationDetail(raw: unknown): string | undefined {
|
|
1148
|
+
// JSON.stringify yields undefined for undefined/functions/symbols despite its typing.
|
|
1149
|
+
const json = JSON.stringify(raw) as string | undefined;
|
|
1150
|
+
return json === undefined ? undefined : json.slice(0, 40);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** Canonical decimal grammar for `iteration` (TD-375): ASCII digits, safe integer. */
|
|
1154
|
+
function parseIteration(raw: string): number | null {
|
|
1155
|
+
if (!/^[0-9]+$/.test(raw)) return null;
|
|
1156
|
+
const value = Number(raw);
|
|
1157
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function classifyIteration(verdict: string, rawIteration: unknown): AdversarialVerdictResult {
|
|
1161
|
+
if (typeof rawIteration !== 'string') {
|
|
1162
|
+
const detail = iterationDetail(rawIteration);
|
|
1163
|
+
return detail === undefined
|
|
1164
|
+
? { kind: 'malformed', reason: 'iteration_invalid' }
|
|
1165
|
+
: { kind: 'malformed', reason: 'iteration_invalid', detail };
|
|
1166
|
+
}
|
|
1167
|
+
const iteration = parseIteration(rawIteration);
|
|
1168
|
+
if (iteration === null) {
|
|
1169
|
+
return { kind: 'iteration_rejected', verdict, detail: iterationDetail(rawIteration) ?? '' };
|
|
1170
|
+
}
|
|
1171
|
+
return { kind: 'ok', verdict, iteration };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function classifyAdversarialBody(body: unknown): AdversarialVerdictResult {
|
|
1175
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
1176
|
+
return { kind: 'malformed', reason: 'body_not_object' };
|
|
1177
|
+
}
|
|
1178
|
+
const metadata: unknown = 'metadata' in body ? body.metadata : undefined;
|
|
1179
|
+
const customMetadata: unknown =
|
|
1180
|
+
typeof metadata === 'object' && metadata !== null && 'customMetadata' in metadata
|
|
1181
|
+
? metadata.customMetadata
|
|
1182
|
+
: undefined;
|
|
1183
|
+
if (typeof customMetadata !== 'object' || customMetadata === null) {
|
|
1184
|
+
return { kind: 'malformed', reason: 'custom_metadata_missing' };
|
|
1185
|
+
}
|
|
1186
|
+
const verdict: unknown = 'verdict' in customMetadata ? customMetadata.verdict : undefined;
|
|
1187
|
+
if (typeof verdict !== 'string') return { kind: 'malformed', reason: 'verdict_invalid' };
|
|
1188
|
+
const rawIteration: unknown =
|
|
1189
|
+
'iteration' in customMetadata ? customMetadata.iteration : undefined;
|
|
1190
|
+
return classifyIteration(verdict, rawIteration);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1091
1193
|
/**
|
|
1092
1194
|
* Retrieves the adversarial review verdict and iteration for a workflow.
|
|
1093
|
-
* Calls GET /api/v1/artifact-content/:workflowId/adversarial_review
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
* artifact is absent). The pre-TD-331 flat metadata.{verdict,iteration}
|
|
1100
|
-
* shape is NOT read (RF-003).
|
|
1195
|
+
* Calls GET /api/v1/artifact-content/:workflowId/adversarial_review and
|
|
1196
|
+
* classifies the outcome (see AdversarialVerdictResult); it never rejects and
|
|
1197
|
+
* never returns null. The API serves the fields in metadata.customMetadata
|
|
1198
|
+
* (strings: S3 x-amz-meta-* headers only carry strings), so `iteration` is
|
|
1199
|
+
* accepted only as a canonical decimal string (/^[0-9]+$/, safe integer). The
|
|
1200
|
+
* pre-TD-331 flat metadata.{verdict,iteration} shape is NOT read (RF-003).
|
|
1101
1201
|
*/
|
|
1102
1202
|
export async function getAdversarialVerdict(
|
|
1103
1203
|
workflowId: string,
|
|
1104
1204
|
token: string
|
|
1105
|
-
): Promise<
|
|
1205
|
+
): Promise<AdversarialVerdictResult> {
|
|
1106
1206
|
try {
|
|
1107
|
-
const
|
|
1108
|
-
content
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
if (
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1207
|
+
const got = await apiGetWithStatus(
|
|
1208
|
+
`/api/v1/artifact-content/${workflowId}/adversarial_review`,
|
|
1209
|
+
token
|
|
1210
|
+
);
|
|
1211
|
+
if (got.kind === 'error') return got;
|
|
1212
|
+
if (got.resp.status === 404) return { kind: 'absent' };
|
|
1213
|
+
if (!got.resp.ok) return { kind: 'error', reason: 'http', status: got.resp.status };
|
|
1214
|
+
let body: unknown;
|
|
1215
|
+
try {
|
|
1216
|
+
body = await got.resp.json();
|
|
1217
|
+
} catch {
|
|
1218
|
+
return { kind: 'malformed', reason: 'body_not_json' };
|
|
1219
|
+
}
|
|
1220
|
+
return classifyAdversarialBody(body);
|
|
1118
1221
|
} catch {
|
|
1119
|
-
return
|
|
1222
|
+
return { kind: 'error', reason: 'unexpected' };
|
|
1120
1223
|
}
|
|
1121
1224
|
}
|
|
1122
1225
|
|
package/hooks/lib/gate-logic.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export type RiskLevel = 'HIGH' | 'LOW';
|
|
8
|
-
export type GateStrategy = '
|
|
8
|
+
export type GateStrategy = 'ALLOW_LOW_RISK' | 'ASK_HUMAN' | 'BLOCK_AND_REQUIRE';
|
|
9
9
|
|
|
10
10
|
export interface GateContext {
|
|
11
11
|
workflowId: string;
|
|
@@ -38,7 +38,7 @@ export function classifyRisk(targetPhase: string): RiskLevel {
|
|
|
38
38
|
* Derive the gate strategy based on context.
|
|
39
39
|
* Rules (evaluated in order):
|
|
40
40
|
* 1. HIGH risk → ASK_HUMAN
|
|
41
|
-
* 2. default →
|
|
41
|
+
* 2. default → ALLOW_LOW_RISK (allowed without an approval check; the hook observes no approval)
|
|
42
42
|
*
|
|
43
43
|
* BLOCK_AND_REQUIRE exists as a type but has no trigger rule yet.
|
|
44
44
|
*/
|
|
@@ -50,9 +50,10 @@ export function deriveGateStrategy(ctx: GateContext): GateDecision {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Default:
|
|
53
|
+
// Default: low-risk phases pass without any approval check (TD-373: the label
|
|
54
|
+
// must not claim an approval this hook never observed).
|
|
54
55
|
return {
|
|
55
|
-
strategy: '
|
|
56
|
-
reason: `Phase '${ctx.targetPhase}' is low-risk —
|
|
56
|
+
strategy: 'ALLOW_LOW_RISK',
|
|
57
|
+
reason: `Phase '${ctx.targetPhase}' is low-risk — allowed without an approval check; no approval observed by this hook.`,
|
|
57
58
|
};
|
|
58
59
|
}
|
package/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "orchestrator-definitions/v1",
|
|
3
3
|
"namespace": "orchestrator",
|
|
4
|
-
"version": "3.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "3.44.0",
|
|
5
|
+
"generatedAt": "2026-10-01T23:00:12.417Z",
|
|
6
6
|
"definitions": {
|
|
7
7
|
"agents": [
|
|
8
8
|
{
|
|
@@ -613,7 +613,7 @@
|
|
|
613
613
|
"slug": "gate-guardian",
|
|
614
614
|
"file": "hooks/gate-guardian.mjs",
|
|
615
615
|
"version": "1.0.0",
|
|
616
|
-
"checksum": "sha256:
|
|
616
|
+
"checksum": "sha256:2186f91f00fe6ec0cd6b49336fb45d9041b5c1fa91d7874a06b58defe1ca04ff",
|
|
617
617
|
"supersedes": null,
|
|
618
618
|
"metadata": {}
|
|
619
619
|
},
|
package/package.json
CHANGED