@highflame/policy 2.1.40 → 2.1.42

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * AARM-Aware Annotation Parser (TypeScript)
3
+ *
4
+ * Runtime layer over the generated AARM annotation registry
5
+ * (aarm-annotations.gen.ts). Ported from packages/go/aarm_annotation.go to
6
+ * give Studio's policy editor the SAME parse + validation rules Shield runs
7
+ * at policy sync time (Go's ValidateAARMAnnotations). The two
8
+ * implementations share a test corpus — see aarm-annotation.test.ts and its
9
+ * Go twin aarm_annotation_test.go.
10
+ *
11
+ * Takes the `Record<string, string>` of a policy's @-annotations and
12
+ * produces:
13
+ *
14
+ * - typed directives Studio (and any TS consumer) can switch on
15
+ * (StepUpRequiredDirective, DeferBelowConfidenceDirective, ...) and
16
+ * - a list of typed validation errors (AARMAnnotationError).
17
+ *
18
+ * Three encoding shapes are accepted, matching the value_encoding block in
19
+ * schemas/annotations.json:
20
+ *
21
+ * 1. Marker: `@defer_on_conflict("")` → no params
22
+ * 2. Single-param positional: `@defer_below_confidence("0.7")` → bare value
23
+ * binds to the single required-positional parameter declared in the
24
+ * registry. Fails closed if no such parameter exists.
25
+ * 3. Multi-param key=value: `@step_up_required("role=finance_lead,timeout_seconds=3600")`
26
+ * parameters are comma-separated key=value pairs. Unknown keys error;
27
+ * missing required parameters error.
28
+ *
29
+ * Generic Cedar annotations (@id, @name, @severity, @tags, arbitrary custom
30
+ * KV pairs) are NOT parsed here — those live in annotations.ts and remain
31
+ * free-form. AARM annotations are a strict subset whose key MUST appear in
32
+ * AARM_ANNOTATION_BY_KEY.
33
+ *
34
+ * Validation is fail-closed (AARM R3/R4): a malformed AARM annotation never
35
+ * silently degrades to a no-op — the caller is told exactly which
36
+ * annotation, which parameter, and what was wrong, so Admin can reject the
37
+ * policy at deploy time and Studio's lint can surface it inline.
38
+ */
39
+ /**
40
+ * Parsed form of `@step_up_required`. Suspends the action pending approval
41
+ * from an approver carrying `role`; fail-closed DENY after `timeoutSeconds`.
42
+ */
43
+ export interface StepUpRequiredDirective {
44
+ /** AuthN role the approver must carry (validated against authn.roles at deploy). */
45
+ role: string;
46
+ /** Seconds the action waits before fail-closed DENY. Defaults to the registry default (86400). */
47
+ timeoutSeconds: number;
48
+ }
49
+ /** Parsed form of `@defer_on_conflict`. Marker — presence is the signal. */
50
+ export interface DeferOnConflictDirective {
51
+ }
52
+ /** Parsed form of `@defer_below_confidence`. Defers when a detector confidence < threshold. */
53
+ export interface DeferBelowConfidenceDirective {
54
+ /** Threshold in [0.0, 1.0]. Detector scores strictly below trigger a deferral. */
55
+ threshold: number;
56
+ }
57
+ /** Parsed form of `@defer_until_context`. Defers when the named context field is empty/missing. */
58
+ export interface DeferUntilContextDirective {
59
+ /** Dotted Cedar context-attribute path (e.g. "session_max_sensitivity"). */
60
+ field: string;
61
+ }
62
+ /**
63
+ * Every typed AARM directive parsed from a single policy's annotation map.
64
+ * Each field is present iff the corresponding annotation was present and
65
+ * parsed successfully.
66
+ */
67
+ export interface AARMDirectives {
68
+ stepUpRequired?: StepUpRequiredDirective;
69
+ deferOnConflict?: DeferOnConflictDirective;
70
+ deferBelowConfidence?: DeferBelowConfidenceDirective;
71
+ deferUntilContext?: DeferUntilContextDirective;
72
+ }
73
+ /** True iff at least one AARM directive was parsed. */
74
+ export declare function hasAnyAARMDirective(d: AARMDirectives | null | undefined): boolean;
75
+ export interface AARMAnnotationErrorInit {
76
+ /** Annotation key (e.g. "step_up_required"). */
77
+ key: string;
78
+ /** Parameter where the failure occurred; omit for annotation-level failures. */
79
+ parameter?: string;
80
+ /** Raw Cedar value (or parameter substring) that was rejected. */
81
+ rawValue?: string;
82
+ /** Short, actionable human-readable message. */
83
+ reason: string;
84
+ }
85
+ /**
86
+ * Structured error for a malformed AARM annotation. Mirrors Go's
87
+ * AARMAnnotationError. Studio's lint surfaces these inline; Admin serializes
88
+ * them in the policy-create rejection.
89
+ */
90
+ export declare class AARMAnnotationError extends Error {
91
+ readonly key: string;
92
+ readonly parameter: string;
93
+ readonly rawValue: string;
94
+ readonly reason: string;
95
+ constructor(init: AARMAnnotationErrorInit);
96
+ }
97
+ /** True iff err is an AARMAnnotationError. */
98
+ export declare function isAARMAnnotationError(err: unknown): err is AARMAnnotationError;
99
+ export interface ParseAARMResult {
100
+ directives: AARMDirectives;
101
+ errors: AARMAnnotationError[];
102
+ }
103
+ /**
104
+ * Walks the raw annotation map and extracts every AARM-recognized annotation
105
+ * into typed directives. Non-AARM keys are ignored (handled by the generic
106
+ * annotation surface in annotations.ts).
107
+ *
108
+ * Errors are COLLECTED, not fail-fast, so a single call surfaces every
109
+ * offending annotation at once — Studio shows all squiggles in one pass.
110
+ * Keys are processed in sorted order for stable error reporting.
111
+ */
112
+ export declare function parseAARMAnnotations(raw: Record<string, string> | null | undefined): ParseAARMResult;
113
+ /**
114
+ * Validate-only variant: returns the error list without the typed
115
+ * directives. Suitable for Admin's policy-create endpoint, which only needs
116
+ * the yes/no verdict + error list.
117
+ */
118
+ export declare function validateAARMAnnotations(raw: Record<string, string> | null | undefined): AARMAnnotationError[];
119
+ /** True iff key is a platform-recognized AARM annotation. */
120
+ export declare function isAARMAnnotationKey(key: string): boolean;
@@ -0,0 +1,494 @@
1
+ /**
2
+ * AARM-Aware Annotation Parser (TypeScript)
3
+ *
4
+ * Runtime layer over the generated AARM annotation registry
5
+ * (aarm-annotations.gen.ts). Ported from packages/go/aarm_annotation.go to
6
+ * give Studio's policy editor the SAME parse + validation rules Shield runs
7
+ * at policy sync time (Go's ValidateAARMAnnotations). The two
8
+ * implementations share a test corpus — see aarm-annotation.test.ts and its
9
+ * Go twin aarm_annotation_test.go.
10
+ *
11
+ * Takes the `Record<string, string>` of a policy's @-annotations and
12
+ * produces:
13
+ *
14
+ * - typed directives Studio (and any TS consumer) can switch on
15
+ * (StepUpRequiredDirective, DeferBelowConfidenceDirective, ...) and
16
+ * - a list of typed validation errors (AARMAnnotationError).
17
+ *
18
+ * Three encoding shapes are accepted, matching the value_encoding block in
19
+ * schemas/annotations.json:
20
+ *
21
+ * 1. Marker: `@defer_on_conflict("")` → no params
22
+ * 2. Single-param positional: `@defer_below_confidence("0.7")` → bare value
23
+ * binds to the single required-positional parameter declared in the
24
+ * registry. Fails closed if no such parameter exists.
25
+ * 3. Multi-param key=value: `@step_up_required("role=finance_lead,timeout_seconds=3600")`
26
+ * parameters are comma-separated key=value pairs. Unknown keys error;
27
+ * missing required parameters error.
28
+ *
29
+ * Generic Cedar annotations (@id, @name, @severity, @tags, arbitrary custom
30
+ * KV pairs) are NOT parsed here — those live in annotations.ts and remain
31
+ * free-form. AARM annotations are a strict subset whose key MUST appear in
32
+ * AARM_ANNOTATION_BY_KEY.
33
+ *
34
+ * Validation is fail-closed (AARM R3/R4): a malformed AARM annotation never
35
+ * silently degrades to a no-op — the caller is told exactly which
36
+ * annotation, which parameter, and what was wrong, so Admin can reject the
37
+ * policy at deploy time and Studio's lint can surface it inline.
38
+ */
39
+ import { AARM_ANNOTATION_BY_KEY, } from './aarm-annotations.gen.js';
40
+ /** True iff at least one AARM directive was parsed. */
41
+ export function hasAnyAARMDirective(d) {
42
+ if (!d)
43
+ return false;
44
+ return (d.stepUpRequired !== undefined ||
45
+ d.deferOnConflict !== undefined ||
46
+ d.deferBelowConfidence !== undefined ||
47
+ d.deferUntilContext !== undefined);
48
+ }
49
+ /**
50
+ * Structured error for a malformed AARM annotation. Mirrors Go's
51
+ * AARMAnnotationError. Studio's lint surfaces these inline; Admin serializes
52
+ * them in the policy-create rejection.
53
+ */
54
+ export class AARMAnnotationError extends Error {
55
+ key;
56
+ parameter;
57
+ rawValue;
58
+ reason;
59
+ constructor(init) {
60
+ const key = init.key;
61
+ const parameter = init.parameter ?? '';
62
+ const rawValue = init.rawValue ?? '';
63
+ const reason = init.reason;
64
+ // Message format matches Go's AARMAnnotationError.Error().
65
+ const message = parameter !== ''
66
+ ? `AARM annotation "${key}" parameter "${parameter}": ${reason} (got "${rawValue}")`
67
+ : `AARM annotation "${key}": ${reason} (got "${rawValue}")`;
68
+ super(message);
69
+ this.name = 'AARMAnnotationError';
70
+ this.key = key;
71
+ this.parameter = parameter;
72
+ this.rawValue = rawValue;
73
+ this.reason = reason;
74
+ Object.setPrototypeOf(this, AARMAnnotationError.prototype);
75
+ }
76
+ }
77
+ /** True iff err is an AARMAnnotationError. */
78
+ export function isAARMAnnotationError(err) {
79
+ return err instanceof AARMAnnotationError;
80
+ }
81
+ /**
82
+ * Walks the raw annotation map and extracts every AARM-recognized annotation
83
+ * into typed directives. Non-AARM keys are ignored (handled by the generic
84
+ * annotation surface in annotations.ts).
85
+ *
86
+ * Errors are COLLECTED, not fail-fast, so a single call surfaces every
87
+ * offending annotation at once — Studio shows all squiggles in one pass.
88
+ * Keys are processed in sorted order for stable error reporting.
89
+ */
90
+ export function parseAARMAnnotations(raw) {
91
+ const directives = {};
92
+ const errors = [];
93
+ if (!raw) {
94
+ return { directives, errors };
95
+ }
96
+ const keys = Object.keys(raw).sort();
97
+ for (const key of keys) {
98
+ const def = AARM_ANNOTATION_BY_KEY[key];
99
+ if (!def) {
100
+ continue; // not an AARM-recognized annotation; ignore
101
+ }
102
+ const value = raw[key];
103
+ const parsed = parseAARMAnnotationParams(def, value);
104
+ if (parsed.error) {
105
+ errors.push(parsed.error);
106
+ continue;
107
+ }
108
+ const params = parsed.params;
109
+ switch (def.key) {
110
+ case 'step_up_required': {
111
+ const r = buildStepUpRequired(def, params, value);
112
+ if (r.error)
113
+ errors.push(r.error);
114
+ else
115
+ directives.stepUpRequired = r.directive;
116
+ break;
117
+ }
118
+ case 'defer_on_conflict':
119
+ directives.deferOnConflict = {};
120
+ break;
121
+ case 'defer_below_confidence': {
122
+ const r = buildDeferBelowConfidence(def, params, value);
123
+ if (r.error)
124
+ errors.push(r.error);
125
+ else
126
+ directives.deferBelowConfidence = r.directive;
127
+ break;
128
+ }
129
+ case 'defer_until_context': {
130
+ const r = buildDeferUntilContext(def, params, value);
131
+ if (r.error)
132
+ errors.push(r.error);
133
+ else
134
+ directives.deferUntilContext = r.directive;
135
+ break;
136
+ }
137
+ default:
138
+ // Registry entry exists but no typed extractor is wired here — a
139
+ // programming error in highflame-policy. Fail closed at runtime.
140
+ errors.push(new AARMAnnotationError({
141
+ key,
142
+ rawValue: value,
143
+ reason: 'registry entry exists but no typed extractor is wired in ' +
144
+ 'aarm-annotation.ts; this is a programming error in highflame-policy',
145
+ }));
146
+ }
147
+ }
148
+ return { directives, errors };
149
+ }
150
+ /**
151
+ * Validate-only variant: returns the error list without the typed
152
+ * directives. Suitable for Admin's policy-create endpoint, which only needs
153
+ * the yes/no verdict + error list.
154
+ */
155
+ export function validateAARMAnnotations(raw) {
156
+ return parseAARMAnnotations(raw).errors;
157
+ }
158
+ /** True iff key is a platform-recognized AARM annotation. */
159
+ export function isAARMAnnotationKey(key) {
160
+ return Object.prototype.hasOwnProperty.call(AARM_ANNOTATION_BY_KEY, key);
161
+ }
162
+ // Hard cap on a raw annotation value's length, enforced before any regex runs.
163
+ // AARM annotation values are short by construction — a role + timeout, a
164
+ // threshold, a dotted context-field path; the longest legitimate value is well
165
+ // under 100 chars. Bounding length here keeps every regex below (including the
166
+ // backtracking float matcher and the registry-sourced `defer_until_context`
167
+ // pattern) in trivially-linear territory, closing a ReDoS vector that would
168
+ // otherwise be reachable from policy-author input — this parser runs in Studio
169
+ // lint AND Admin's policy-create endpoint. (Go's RE2 + strconv are linear and
170
+ // don't share this hazard, so the cap is a TS-specific guard.)
171
+ const MAX_AARM_VALUE_LEN = 512;
172
+ // Compiled-pattern cache for registry-sourced regexes (defer_until_context's
173
+ // `field` pattern). Registry patterns are build-time, bounded, and immutable
174
+ // for the process lifetime, so compile once and reuse — parity with Go's
175
+ // compileRegistryPattern cache, and avoids recompiling on every parse call.
176
+ const patternCache = new Map();
177
+ /**
178
+ * Normalizes a raw Cedar annotation value into a `name → string-value` map,
179
+ * honoring the three encoding shapes. Per-parameter coercion + bounds happen
180
+ * in the per-directive builders.
181
+ */
182
+ function parseAARMAnnotationParams(def, raw) {
183
+ const values = {};
184
+ // The public type is Record<string,string>, but at runtime a value can be
185
+ // non-string — e.g. `{"step_up_required": null}` from a deserialized policy,
186
+ // or a JS caller. Guard before any string op so a malformed value yields a
187
+ // structured error instead of a TypeError that crashes the validating
188
+ // request (fail-closed, consistent with the rest of the parser).
189
+ if (typeof raw !== 'string') {
190
+ return {
191
+ params: {},
192
+ error: new AARMAnnotationError({
193
+ key: def.key,
194
+ rawValue: raw === undefined ? 'undefined' : raw === null ? 'null' : String(raw),
195
+ reason: 'annotation value must be a string',
196
+ }),
197
+ };
198
+ }
199
+ if (raw.length > MAX_AARM_VALUE_LEN) {
200
+ return {
201
+ params: {},
202
+ error: new AARMAnnotationError({
203
+ key: def.key,
204
+ // Truncate so the error itself doesn't echo the oversized value.
205
+ rawValue: `${raw.slice(0, 64)}…`,
206
+ reason: `annotation value exceeds the maximum length of ${MAX_AARM_VALUE_LEN} characters`,
207
+ }),
208
+ };
209
+ }
210
+ // Marker annotation — Cedar requires a string value, so authors pass
211
+ // anything; PRESENCE is the signal.
212
+ if (def.parameters.length === 0) {
213
+ return { params: values };
214
+ }
215
+ if (raw.includes('=')) {
216
+ // Multi-param k=v (also covers a single named param).
217
+ for (const segment of raw.split(',')) {
218
+ const seg = segment.trim();
219
+ if (seg === '')
220
+ continue;
221
+ const eq = seg.indexOf('=');
222
+ if (eq <= 0 || eq === seg.length - 1) {
223
+ return {
224
+ params: {},
225
+ error: new AARMAnnotationError({
226
+ key: def.key,
227
+ rawValue: raw,
228
+ reason: `malformed key=value pair "${seg}"`,
229
+ }),
230
+ };
231
+ }
232
+ const k = seg.slice(0, eq).trim();
233
+ const v = seg.slice(eq + 1).trim();
234
+ if (!hasParameter(def, k)) {
235
+ return {
236
+ params: {},
237
+ error: new AARMAnnotationError({
238
+ key: def.key,
239
+ parameter: k,
240
+ rawValue: v,
241
+ reason: `unknown parameter; allowed: ${parameterNames(def).join(', ')}`,
242
+ }),
243
+ };
244
+ }
245
+ values[k] = v;
246
+ }
247
+ return { params: values };
248
+ }
249
+ // Single-positional shorthand. Bind the whole value to the positional param.
250
+ const pos = positionalParameter(def);
251
+ if (!pos) {
252
+ return {
253
+ params: {},
254
+ error: new AARMAnnotationError({
255
+ key: def.key,
256
+ rawValue: raw,
257
+ reason: 'bare value provided but the annotation has no positional parameter; ' +
258
+ 'use the key=value form',
259
+ }),
260
+ };
261
+ }
262
+ values[pos.name] = raw.trim();
263
+ return { params: values };
264
+ }
265
+ function buildStepUpRequired(def, params, raw) {
266
+ const role = requireStringParam(def, params, 'role', raw);
267
+ if ('error' in role)
268
+ return { error: role.error };
269
+ const timeoutDef = mustLookupParam(def, 'timeout_seconds');
270
+ const defaultTimeout = timeoutDef.default && timeoutDef.default.kind === 'int' ? timeoutDef.default.value : 0;
271
+ const timeout = optionalIntParam(def, params, 'timeout_seconds', defaultTimeout, raw);
272
+ if ('error' in timeout)
273
+ return { error: timeout.error };
274
+ const bounds = checkIntBounds(def, 'timeout_seconds', timeout.value, timeoutDef, raw);
275
+ if (bounds)
276
+ return { error: bounds };
277
+ return { directive: { role: role.value, timeoutSeconds: timeout.value } };
278
+ }
279
+ function buildDeferBelowConfidence(def, params, raw) {
280
+ const thresholdDef = mustLookupParam(def, 'threshold');
281
+ const threshold = requireFloatParam(def, params, 'threshold', raw);
282
+ if ('error' in threshold)
283
+ return { error: threshold.error };
284
+ const bounds = checkFloatBounds(def, 'threshold', threshold.value, thresholdDef, raw);
285
+ if (bounds)
286
+ return { error: bounds };
287
+ return { directive: { threshold: threshold.value } };
288
+ }
289
+ function buildDeferUntilContext(def, params, raw) {
290
+ const field = requireStringParam(def, params, 'field', raw);
291
+ if ('error' in field)
292
+ return { error: field.error };
293
+ const fieldDef = mustLookupParam(def, 'field');
294
+ if (fieldDef.pattern !== '') {
295
+ let re = patternCache.get(fieldDef.pattern);
296
+ if (!re) {
297
+ try {
298
+ re = new RegExp(fieldDef.pattern);
299
+ }
300
+ catch (e) {
301
+ return {
302
+ error: new AARMAnnotationError({
303
+ key: def.key,
304
+ parameter: 'field',
305
+ rawValue: field.value,
306
+ reason: `registry pattern "${fieldDef.pattern}" does not compile: ${String(e)} (registry bug)`,
307
+ }),
308
+ };
309
+ }
310
+ patternCache.set(fieldDef.pattern, re);
311
+ }
312
+ if (!re.test(field.value)) {
313
+ return {
314
+ error: new AARMAnnotationError({
315
+ key: def.key,
316
+ parameter: 'field',
317
+ rawValue: field.value,
318
+ reason: `value does not match required pattern "${fieldDef.pattern}" (Cedar context-attribute path)`,
319
+ }),
320
+ };
321
+ }
322
+ }
323
+ return { directive: { field: field.value } };
324
+ }
325
+ // Go's strconv.ParseFloat accepts NaN/Inf as valid float SYNTAX; we reject
326
+ // them as non-finite (parity with requireFloatParam's finite-number gate).
327
+ const NON_FINITE_RE = /^[+-]?(nan|inf|infinity)$/i;
328
+ const FLOAT_RE = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
329
+ const INT_RE = /^[+-]?\d+$/;
330
+ function requireStringParam(def, params, name, raw) {
331
+ const v = params[name];
332
+ if (v === undefined || v === '') {
333
+ return {
334
+ error: new AARMAnnotationError({
335
+ key: def.key,
336
+ parameter: name,
337
+ rawValue: raw,
338
+ reason: 'required parameter is missing',
339
+ }),
340
+ };
341
+ }
342
+ return { value: v };
343
+ }
344
+ function requireFloatParam(def, params, name, raw) {
345
+ const v = params[name];
346
+ if (v === undefined || v === '') {
347
+ return {
348
+ error: new AARMAnnotationError({
349
+ key: def.key,
350
+ parameter: name,
351
+ rawValue: raw,
352
+ reason: 'required parameter is missing',
353
+ }),
354
+ };
355
+ }
356
+ // AARM R3/R4 fail-closed: NaN/Inf parse as "valid float syntax" in Go and
357
+ // would slip past bounds checks (NaN comparisons are uniformly false),
358
+ // letting Shield emit a directive that never fires. Reject up front.
359
+ if (NON_FINITE_RE.test(v)) {
360
+ return {
361
+ error: new AARMAnnotationError({
362
+ key: def.key,
363
+ parameter: name,
364
+ rawValue: v,
365
+ reason: 'value must be a finite number (NaN, +Inf, -Inf are rejected)',
366
+ }),
367
+ };
368
+ }
369
+ if (!FLOAT_RE.test(v)) {
370
+ return {
371
+ error: new AARMAnnotationError({
372
+ key: def.key,
373
+ parameter: name,
374
+ rawValue: v,
375
+ reason: 'value is not a valid float',
376
+ }),
377
+ };
378
+ }
379
+ const f = Number(v);
380
+ if (!Number.isFinite(f)) {
381
+ return {
382
+ error: new AARMAnnotationError({
383
+ key: def.key,
384
+ parameter: name,
385
+ rawValue: v,
386
+ reason: 'value must be a finite number (NaN, +Inf, -Inf are rejected)',
387
+ }),
388
+ };
389
+ }
390
+ return { value: f };
391
+ }
392
+ function optionalIntParam(def, params, name, defaultValue, raw) {
393
+ const v = params[name];
394
+ if (v === undefined || v === '') {
395
+ return { value: defaultValue };
396
+ }
397
+ if (!INT_RE.test(v)) {
398
+ return {
399
+ error: new AARMAnnotationError({
400
+ key: def.key,
401
+ parameter: name,
402
+ rawValue: v,
403
+ reason: 'value is not a valid int',
404
+ }),
405
+ };
406
+ }
407
+ return { value: Number(v) };
408
+ }
409
+ function checkFloatBounds(def, name, value, pdef, raw) {
410
+ // Belt-and-braces with requireFloatParam: a non-finite value reaching here
411
+ // (via a future code path that skips it) must still be rejected.
412
+ if (!Number.isFinite(value)) {
413
+ return new AARMAnnotationError({
414
+ key: def.key,
415
+ parameter: name,
416
+ rawValue: raw,
417
+ reason: 'value must be a finite number (NaN, +Inf, -Inf are rejected)',
418
+ });
419
+ }
420
+ if (pdef.min !== null) {
421
+ const min = pdef.min.value;
422
+ if (value < min) {
423
+ return new AARMAnnotationError({
424
+ key: def.key,
425
+ parameter: name,
426
+ rawValue: raw,
427
+ reason: `value ${value} is below the minimum ${min}`,
428
+ });
429
+ }
430
+ }
431
+ if (pdef.max !== null) {
432
+ const max = pdef.max.value;
433
+ if (value > max) {
434
+ return new AARMAnnotationError({
435
+ key: def.key,
436
+ parameter: name,
437
+ rawValue: raw,
438
+ reason: `value ${value} is above the maximum ${max}`,
439
+ });
440
+ }
441
+ }
442
+ return undefined;
443
+ }
444
+ function checkIntBounds(def, name, value, pdef, raw) {
445
+ if (pdef.min !== null) {
446
+ const min = pdef.min.value;
447
+ if (value < min) {
448
+ return new AARMAnnotationError({
449
+ key: def.key,
450
+ parameter: name,
451
+ rawValue: raw,
452
+ reason: `value ${value} is below the minimum ${min}`,
453
+ });
454
+ }
455
+ }
456
+ if (pdef.max !== null) {
457
+ const max = pdef.max.value;
458
+ if (value > max) {
459
+ return new AARMAnnotationError({
460
+ key: def.key,
461
+ parameter: name,
462
+ rawValue: raw,
463
+ reason: `value ${value} is above the maximum ${max}`,
464
+ });
465
+ }
466
+ }
467
+ return undefined;
468
+ }
469
+ // ─────────────────────────────────────────────────────────────────────────
470
+ // Registry-introspection helpers
471
+ // ─────────────────────────────────────────────────────────────────────────
472
+ function hasParameter(def, name) {
473
+ return def.parameters.some((p) => p.name === name);
474
+ }
475
+ function parameterNames(def) {
476
+ return def.parameters.map((p) => p.name);
477
+ }
478
+ function positionalParameter(def) {
479
+ return def.parameters.find((p) => p.positional);
480
+ }
481
+ /**
482
+ * Returns the named parameter def; throws if absent. Used only for
483
+ * parameters the builder REQUIRES the registry to declare — a missing entry
484
+ * means aarm-annotation.ts drifted from schemas/annotations.json, and a loud
485
+ * throw surfaces it at the test boundary rather than a later undefined-read.
486
+ */
487
+ function mustLookupParam(def, name) {
488
+ const p = def.parameters.find((x) => x.name === name);
489
+ if (!p) {
490
+ throw new Error(`aarm-annotation.ts: registry entry for "${def.key}" does not declare ` +
491
+ `parameter "${name}" (schemas/annotations.json drift)`);
492
+ }
493
+ return p;
494
+ }
@@ -66,7 +66,7 @@ export const AARM_ANNOTATIONS = [
66
66
  },
67
67
  {
68
68
  key: 'step_up_required',
69
- description: 'Suspend the action pending human approval from an approver with the named role. AARM R4 STEP_UP decision: action does not execute until POST /v1/approvals/{id}/resolve returns allow, OR timeout_seconds elapses (fail-closed: timeout DENYs the action, never permits).',
69
+ description: 'Suspend the action pending human approval from an approver with the named role. AARM R4 STEP_UP decision: Shield issues an OpenID CIBA bc-authorize challenge and the action does not execute until an approver carrying the role resolves it (POST /oauth2/bc-authorize/{auth_req_id}/approve via AuthN), OR timeout_seconds elapses (fail-closed: timeout DENYs the action, never permits).',
70
70
  aarmRequirement: 'R4',
71
71
  promotesCapability: 'CAP-ENF-004',
72
72
  decisionEffect: 'step_up',
@@ -30,7 +30,9 @@ export declare const AiGatewayContextKey: {
30
30
  readonly PiiDetected: "pii_detected";
31
31
  readonly PiiScore: "pii_score";
32
32
  readonly PiiTypes: "pii_types";
33
+ readonly PrivilegeScope: "privilege_scope";
33
34
  readonly ProfanityScore: "profanity_score";
35
+ readonly Role: "role";
34
36
  readonly RugPullDetected: "rug_pull_detected";
35
37
  readonly RugPullScore: "rug_pull_score";
36
38
  readonly SecretCount: "secret_count";
@@ -58,6 +60,7 @@ export declare const AiGatewayContextKey: {
58
60
  readonly ToolIsBuiltin: "tool_is_builtin";
59
61
  readonly ToolIsSensitive: "tool_is_sensitive";
60
62
  readonly ToolName: "tool_name";
63
+ readonly ToolOperationClasses: "tool_operation_classes";
61
64
  readonly ToolPoisoningDetected: "tool_poisoning_detected";
62
65
  readonly ToolPoisoningScore: "tool_poisoning_score";
63
66
  readonly ToolRiskScore: "tool_risk_score";
@@ -32,7 +32,9 @@ export const AiGatewayContextKey = {
32
32
  PiiDetected: 'pii_detected',
33
33
  PiiScore: 'pii_score',
34
34
  PiiTypes: 'pii_types',
35
+ PrivilegeScope: 'privilege_scope',
35
36
  ProfanityScore: 'profanity_score',
37
+ Role: 'role',
36
38
  RugPullDetected: 'rug_pull_detected',
37
39
  RugPullScore: 'rug_pull_score',
38
40
  SecretCount: 'secret_count',
@@ -60,6 +62,7 @@ export const AiGatewayContextKey = {
60
62
  ToolIsBuiltin: 'tool_is_builtin',
61
63
  ToolIsSensitive: 'tool_is_sensitive',
62
64
  ToolName: 'tool_name',
65
+ ToolOperationClasses: 'tool_operation_classes',
63
66
  ToolPoisoningDetected: 'tool_poisoning_detected',
64
67
  ToolPoisoningScore: 'tool_poisoning_score',
65
68
  ToolRiskScore: 'tool_risk_score',
@@ -77,8 +77,10 @@ export declare const GuardrailsContextKey: {
77
77
  readonly PiiDetected: "pii_detected";
78
78
  readonly PiiScore: "pii_score";
79
79
  readonly PiiTypes: "pii_types";
80
+ readonly PrivilegeScope: "privilege_scope";
80
81
  readonly ProfanityScore: "profanity_score";
81
82
  readonly RequestId: "request_id";
83
+ readonly Role: "role";
82
84
  readonly RugPullDetected: "rug_pull_detected";
83
85
  readonly RugPullScore: "rug_pull_score";
84
86
  readonly RugPullType: "rug_pull_type";
@@ -111,6 +113,7 @@ export declare const GuardrailsContextKey: {
111
113
  readonly ToolIsBuiltin: "tool_is_builtin";
112
114
  readonly ToolIsSensitive: "tool_is_sensitive";
113
115
  readonly ToolName: "tool_name";
116
+ readonly ToolOperationClasses: "tool_operation_classes";
114
117
  readonly ToolPoisoningDetected: "tool_poisoning_detected";
115
118
  readonly ToolPoisoningScore: "tool_poisoning_score";
116
119
  readonly ToolPoisoningType: "tool_poisoning_type";