@highflame/policy 2.2.45 → 2.4.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/_schemas/agent_ops/schema.cedarschema +22 -0
- package/_schemas/ai_gateway/schema.cedarschema +49 -0
- package/_schemas/ai_gateway/templates/single_use_sensitive_tools.cedar +54 -0
- package/_schemas/ai_gateway/templates/templates.json +37 -0
- package/_schemas/ai_gateway/templates/unadopted_agents.cedar +64 -0
- package/_schemas/guardrails/schema.cedarschema +21 -0
- package/_schemas/guardrails/templates/dual_attribution.cedar +143 -48
- package/_schemas/guardrails/templates/single_use_sensitive_tools.cedar +54 -0
- package/_schemas/guardrails/templates/templates.json +34 -2
- package/_schemas/guardrails/templates/unadopted_agents.cedar +64 -0
- package/dist/aarm-annotation.d.ts +58 -4
- package/dist/aarm-annotation.js +177 -4
- package/dist/aarm-annotations.gen.js +85 -1
- package/dist/ai_gateway-defaults.gen.d.ts +1 -1
- package/dist/ai_gateway-defaults.gen.js +176 -0
- package/dist/annotations.d.ts +3 -2
- package/dist/annotations.js +6 -2
- package/dist/guardrails-defaults.gen.js +309 -44
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/service-schemas.gen.d.ts +3 -3
- package/dist/service-schemas.gen.js +92 -0
- package/dist/step-up-role-conflict.d.ts +49 -0
- package/dist/step-up-role-conflict.js +117 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Single-Use for Sensitive Tools
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Permits sensitive tool calls, but only once per grant. @single_use makes
|
|
5
|
+
// Shield record the first allow in an atomic consume store and deny any replay
|
|
6
|
+
// of the same grant ("grant_consumed"). Bare @single_use means per tool: one
|
|
7
|
+
// allow covers any call to the same action and tool. Use
|
|
8
|
+
// @single_use("request") instead to bind the allow to one exact set of
|
|
9
|
+
// arguments.
|
|
10
|
+
//
|
|
11
|
+
// @single_use is only valid on a permit. Deny, DEFER and step-up outcomes
|
|
12
|
+
// never consume. If the consume store is unavailable the request is denied
|
|
13
|
+
// ("single_use_store_unavailable") unless the operator opted into fail-open.
|
|
14
|
+
//
|
|
15
|
+
// Requests that carry no bearer token, such as gateway and internal-service
|
|
16
|
+
// traffic, have no token id to record the use against. Shield denies every
|
|
17
|
+
// sensitive call_tool from them ("single_use_unbound"), whatever the
|
|
18
|
+
// fail-open setting. Until the gateway forwards the caller's token to Shield
|
|
19
|
+
// (ADR 0041 D16, ADR 0042 D10), that means every sensitive gateway call.
|
|
20
|
+
//
|
|
21
|
+
// Try it with request-level monitor mode (guard requests sent with
|
|
22
|
+
// mode=monitor): single_use.would_deny on each response shows what enforcing
|
|
23
|
+
// would deny. Per-policy monitor mode does NOT exercise @single_use, because
|
|
24
|
+
// Shield reads it only from enforcing policies; a monitor-mode copy of this
|
|
25
|
+
// template reports nothing, and switching it to enforce takes effect at once.
|
|
26
|
+
//
|
|
27
|
+
// Adopted alone, this permit also allows every sensitive call_tool (once per
|
|
28
|
+
// grant). Pair it with the policies that decide which sensitive calls are
|
|
29
|
+
// allowed at all.
|
|
30
|
+
//
|
|
31
|
+
// Context keys consumed:
|
|
32
|
+
// - tool_is_sensitive: Bool
|
|
33
|
+
//
|
|
34
|
+
// Compliance:
|
|
35
|
+
// - OWASP ASI02
|
|
36
|
+
//
|
|
37
|
+
// Category: agent-identity
|
|
38
|
+
// Namespace: Guardrails
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
@id("agent-identity.permit-single-use-sensitive-tools")
|
|
42
|
+
@name("Permit sensitive tools once per grant")
|
|
43
|
+
@description("Permits call_tool on a sensitive tool once per grant.")
|
|
44
|
+
@severity("medium")
|
|
45
|
+
@tags("category:agent-identity,surface:call-tool,scope:per-tool,owasp:asi02,single-use")
|
|
46
|
+
@single_use
|
|
47
|
+
permit (
|
|
48
|
+
principal,
|
|
49
|
+
action == Guardrails::Action::"call_tool",
|
|
50
|
+
resource
|
|
51
|
+
)
|
|
52
|
+
when {
|
|
53
|
+
context has tool_is_sensitive && context.tool_is_sensitive
|
|
54
|
+
};
|
|
@@ -637,7 +637,7 @@
|
|
|
637
637
|
{
|
|
638
638
|
"id": "agent-identity.dual-attribution",
|
|
639
639
|
"name": "Dual Attribution",
|
|
640
|
-
"description": "
|
|
640
|
+
"description": "Require human approval or an explicit allow grant for sensitive agent tool calls that no human answers for (no acting human, e.g. autonomous or workload agents); calls with an acting human pass. Customize the approver role. Deploy in enforce mode: the approval gate does not exist in monitor mode. Adopt agents first (a service key is unverified until then), or trial the unverified-agent block as its own monitor-mode policy.",
|
|
641
641
|
"category": "agent-identity",
|
|
642
642
|
"file": "dual_attribution.cedar",
|
|
643
643
|
"severity": "critical",
|
|
@@ -647,7 +647,39 @@
|
|
|
647
647
|
"surface:call-tool",
|
|
648
648
|
"scope:per-agent",
|
|
649
649
|
"posture:deny-default",
|
|
650
|
-
"owasp:asi01"
|
|
650
|
+
"owasp:asi01",
|
|
651
|
+
"step-up"
|
|
652
|
+
]
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"id": "agent-identity.block-unadopted-agent-sensitive-tools",
|
|
656
|
+
"name": "Unadopted Agents: No Sensitive Tool Calls",
|
|
657
|
+
"description": "Block sensitive tool calls from agents whose lifecycle status is not active (discovered or pending). Deploy in monitor mode first to see which discovered agents already call sensitive tools.",
|
|
658
|
+
"category": "agent-identity",
|
|
659
|
+
"file": "unadopted_agents.cedar",
|
|
660
|
+
"severity": "high",
|
|
661
|
+
"tags": [
|
|
662
|
+
"category:agent-identity",
|
|
663
|
+
"detection:rule",
|
|
664
|
+
"surface:call-tool",
|
|
665
|
+
"scope:per-agent",
|
|
666
|
+
"posture:deny-default",
|
|
667
|
+
"owasp:asi04"
|
|
668
|
+
]
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"id": "agent-identity.permit-single-use-sensitive-tools",
|
|
672
|
+
"name": "Single-Use for Sensitive Tools",
|
|
673
|
+
"description": "Permit sensitive tool calls once per grant: the first allow is recorded and a replay of the same grant is denied. Bare @single_use means per tool; use @single_use(\"request\") to bind to one set of arguments. Requests without a bearer token (gateway and internal-service traffic, until the gateway forwards the caller token) are denied (single_use_unbound). Try it with request-level mode=monitor: per-policy monitor mode does not exercise @single_use. Adopted alone, it also permits every sensitive call.",
|
|
674
|
+
"category": "agent-identity",
|
|
675
|
+
"file": "single_use_sensitive_tools.cedar",
|
|
676
|
+
"severity": "medium",
|
|
677
|
+
"tags": [
|
|
678
|
+
"category:agent-identity",
|
|
679
|
+
"surface:call-tool",
|
|
680
|
+
"scope:per-tool",
|
|
681
|
+
"owasp:asi02",
|
|
682
|
+
"single-use"
|
|
651
683
|
]
|
|
652
684
|
}
|
|
653
685
|
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Unadopted Agents: No Sensitive Tool Calls
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Agents that an enterprise registers in its own IdP (Entra Agent ID, Okta AI
|
|
5
|
+
// agents) are discovered by Highflame before anyone adopts them. A discovered
|
|
6
|
+
// or pending agent has an identity but no owner-reviewed policy, so it should
|
|
7
|
+
// not reach sensitive tools until it is adopted (status "active").
|
|
8
|
+
//
|
|
9
|
+
// `context.actor.status` carries the agent lifecycle status. When no actor
|
|
10
|
+
// record is present the rule falls back to the legacy `context.principal.status`
|
|
11
|
+
// profile. Absence means "unknown", not "active": the has-guards below make a
|
|
12
|
+
// request with no status fall through to the other rules instead of being
|
|
13
|
+
// matched here. Any status other than "active" is matched, so suspended and
|
|
14
|
+
// deactivated agents are blocked too. The comparison is case-sensitive, like
|
|
15
|
+
// ZeroID's lowercase status values.
|
|
16
|
+
//
|
|
17
|
+
// Today this template is forward-looking. ZeroID mints tokens only for active
|
|
18
|
+
// identities and stamps "active" at mint, and every caller of Shield forwards
|
|
19
|
+
// that value, so no live path reports another status yet. It takes effect when
|
|
20
|
+
// a non-active status reaches Shield, as ADR 0042 plans for agents registered
|
|
21
|
+
// in a customer IdP. The status is a mint-time snapshot: an identity suspended
|
|
22
|
+
// after its token was minted reads "active" until the token expires.
|
|
23
|
+
//
|
|
24
|
+
// `tool_is_sensitive` is set by Shield's tool_validator from a fixed list of
|
|
25
|
+
// tool names (shell, execute_command, write_file, http_post, send_email,
|
|
26
|
+
// delete_file, run_sql, plus Shield's configured list); other tools are not
|
|
27
|
+
// sensitive and are not matched.
|
|
28
|
+
//
|
|
29
|
+
// This copy covers Guardrails traffic. Gateway traffic (Firehog) is evaluated
|
|
30
|
+
// in the AIGateway namespace; use the AIGateway copy for it.
|
|
31
|
+
//
|
|
32
|
+
// Deploy in MONITOR mode first to see which discovered agents already call
|
|
33
|
+
// sensitive tools, adopt them, then switch to enforce.
|
|
34
|
+
//
|
|
35
|
+
// Context keys consumed:
|
|
36
|
+
// - actor, principal: records - status is the agent lifecycle status
|
|
37
|
+
// - tool_is_sensitive: Bool
|
|
38
|
+
//
|
|
39
|
+
// Compliance:
|
|
40
|
+
// - OWASP ASI04
|
|
41
|
+
//
|
|
42
|
+
// Category: agent-identity
|
|
43
|
+
// Namespace: Guardrails
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
@id("agent-identity.block-unadopted-agent-sensitive-tools")
|
|
47
|
+
@name("Block unadopted agents from sensitive tools")
|
|
48
|
+
@description("Blocks call_tool on a sensitive tool when actor.status (or, without an actor record, principal.status) is present and is not active.")
|
|
49
|
+
@severity("high")
|
|
50
|
+
@tags("category:agent-identity,detection:rule,surface:call-tool,scope:per-agent,posture:deny-default,owasp:asi04")
|
|
51
|
+
@reject_message("Tool execution blocked: this agent has not been adopted, so it may not call sensitive tools. Adopt the agent to activate it.")
|
|
52
|
+
forbid (
|
|
53
|
+
principal,
|
|
54
|
+
action == Guardrails::Action::"call_tool",
|
|
55
|
+
resource
|
|
56
|
+
)
|
|
57
|
+
when {
|
|
58
|
+
(
|
|
59
|
+
(context has actor && context.actor has status && context.actor.status != "active") ||
|
|
60
|
+
(!(context has actor) && context has principal && context.principal has status &&
|
|
61
|
+
context.principal.status != "active")
|
|
62
|
+
) &&
|
|
63
|
+
context has tool_is_sensitive && context.tool_is_sensitive
|
|
64
|
+
};
|
|
@@ -76,6 +76,39 @@ export interface RedactionStrategyDirective {
|
|
|
76
76
|
*/
|
|
77
77
|
strategy: string;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Parsed form of `@step_up_scope`. Absent means a step-up approval authorizes
|
|
81
|
+
* exactly one call; "session" lets it cover repeated calls to the same tool
|
|
82
|
+
* for the life of the approval token. Only valid with `@step_up_required`.
|
|
83
|
+
*/
|
|
84
|
+
export interface StepUpScopeDirective {
|
|
85
|
+
/** Always "session" — the only legal value. */
|
|
86
|
+
scope: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parsed form of `@step_up_four_eyes`. Marker: the approver must be a
|
|
90
|
+
* different person from the requesting agent's owner. Written bare or as
|
|
91
|
+
* `@step_up_four_eyes("true")`. Only valid with `@step_up_required`.
|
|
92
|
+
*/
|
|
93
|
+
export interface StepUpFourEyesDirective {
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parsed form of `@step_up_unless_granted`. Marker: an explicit agent grant
|
|
97
|
+
* satisfies this policy's step-up approval. When the call is explicitly
|
|
98
|
+
* allowed by an enforcing allow grant, Shield waives the challenge, provided
|
|
99
|
+
* every step-up policy matching the call carries it. Written bare or as
|
|
100
|
+
* `@step_up_unless_granted("true")`. Only valid with `@step_up_required`.
|
|
101
|
+
*/
|
|
102
|
+
export interface StepUpUnlessGrantedDirective {
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Parsed form of `@single_use`. The allow produced by the permit carrying it
|
|
106
|
+
* can be spent once. Only valid on a permit (see validateAARMAnnotationsForEffect).
|
|
107
|
+
*/
|
|
108
|
+
export interface SingleUseDirective {
|
|
109
|
+
/** "tool" (default; what the bare annotation means) or "request" (same arguments). */
|
|
110
|
+
granularity: string;
|
|
111
|
+
}
|
|
79
112
|
/**
|
|
80
113
|
* Every typed AARM directive parsed from a single policy's annotation map.
|
|
81
114
|
* Each field is present iff the corresponding annotation was present and
|
|
@@ -87,6 +120,10 @@ export interface AARMDirectives {
|
|
|
87
120
|
deferBelowConfidence?: DeferBelowConfidenceDirective;
|
|
88
121
|
deferUntilContext?: DeferUntilContextDirective;
|
|
89
122
|
redactionStrategy?: RedactionStrategyDirective;
|
|
123
|
+
stepUpScope?: StepUpScopeDirective;
|
|
124
|
+
stepUpFourEyes?: StepUpFourEyesDirective;
|
|
125
|
+
stepUpUnlessGranted?: StepUpUnlessGrantedDirective;
|
|
126
|
+
singleUse?: SingleUseDirective;
|
|
90
127
|
}
|
|
91
128
|
/** True iff at least one AARM directive was parsed. */
|
|
92
129
|
export declare function hasAnyAARMDirective(d: AARMDirectives | null | undefined): boolean;
|
|
@@ -127,12 +164,29 @@ export interface ParseAARMResult {
|
|
|
127
164
|
* offending annotation at once — Studio shows all squiggles in one pass.
|
|
128
165
|
* Keys are processed in sorted order for stable error reporting.
|
|
129
166
|
*/
|
|
130
|
-
export declare function parseAARMAnnotations(raw: Record<string, string> | null | undefined): ParseAARMResult;
|
|
167
|
+
export declare function parseAARMAnnotations(raw: Record<string, string | null> | null | undefined): ParseAARMResult;
|
|
168
|
+
/**
|
|
169
|
+
* THE authoring-time AARM check: run it on every rule of a policy before
|
|
170
|
+
* saving it (Studio's lint, Admin's policy-create/update), passing the rule's
|
|
171
|
+
* effect. It is validateAARMAnnotations plus the checks that depend on the
|
|
172
|
+
* effect: `@single_use` is only meaningful on a permit (a forbid never
|
|
173
|
+
* produces an allow to consume). validateAARMAnnotations alone is
|
|
174
|
+
* effect-blind and is not sufficient.
|
|
175
|
+
*
|
|
176
|
+
* Shield runs the same check at sync, but there an error is not always a
|
|
177
|
+
* reject: a permit with an invalid annotation is dropped (fail closed), while
|
|
178
|
+
* a forbid is loaded with the offending annotation ignored, since dropping a
|
|
179
|
+
* forbid would remove a block. Surface the error here, at save time.
|
|
180
|
+
*
|
|
181
|
+
* Exported from both the package root and the browser-safe `./types` entry.
|
|
182
|
+
*/
|
|
183
|
+
export declare function validateAARMAnnotationsForEffect(effect: 'permit' | 'forbid', raw: Record<string, string | null> | null | undefined): AARMAnnotationError[];
|
|
131
184
|
/**
|
|
132
185
|
* Validate-only variant: returns the error list without the typed
|
|
133
|
-
* directives.
|
|
134
|
-
*
|
|
186
|
+
* directives. Effect-blind: authoring checks that know the rule's effect
|
|
187
|
+
* (Admin's policy-create, Studio's lint) must use
|
|
188
|
+
* validateAARMAnnotationsForEffect instead.
|
|
135
189
|
*/
|
|
136
|
-
export declare function validateAARMAnnotations(raw: Record<string, string> | null | undefined): AARMAnnotationError[];
|
|
190
|
+
export declare function validateAARMAnnotations(raw: Record<string, string | null> | null | undefined): AARMAnnotationError[];
|
|
137
191
|
/** True iff key is a platform-recognized AARM annotation. */
|
|
138
192
|
export declare function isAARMAnnotationKey(key: string): boolean;
|
package/dist/aarm-annotation.js
CHANGED
|
@@ -45,7 +45,11 @@ export function hasAnyAARMDirective(d) {
|
|
|
45
45
|
d.deferOnConflict !== undefined ||
|
|
46
46
|
d.deferBelowConfidence !== undefined ||
|
|
47
47
|
d.deferUntilContext !== undefined ||
|
|
48
|
-
d.redactionStrategy !== undefined
|
|
48
|
+
d.redactionStrategy !== undefined ||
|
|
49
|
+
d.stepUpScope !== undefined ||
|
|
50
|
+
d.stepUpFourEyes !== undefined ||
|
|
51
|
+
d.stepUpUnlessGranted !== undefined ||
|
|
52
|
+
d.singleUse !== undefined);
|
|
49
53
|
}
|
|
50
54
|
/**
|
|
51
55
|
* Structured error for a malformed AARM annotation. Mirrors Go's
|
|
@@ -100,7 +104,11 @@ export function parseAARMAnnotations(raw) {
|
|
|
100
104
|
if (!def) {
|
|
101
105
|
continue; // not an AARM-recognized annotation; ignore
|
|
102
106
|
}
|
|
103
|
-
|
|
107
|
+
// cedar-wasm reports a value-less annotation (`@single_use`) as null:
|
|
108
|
+
// treat it as bare, like Go and parseAnnotations do. undefined and
|
|
109
|
+
// non-strings still fail closed in parseAARMAnnotationParams.
|
|
110
|
+
const rawValue = raw[key];
|
|
111
|
+
const value = (rawValue === null ? '' : rawValue);
|
|
104
112
|
const parsed = parseAARMAnnotationParams(def, value);
|
|
105
113
|
if (parsed.error) {
|
|
106
114
|
errors.push(parsed.error);
|
|
@@ -143,6 +151,38 @@ export function parseAARMAnnotations(raw) {
|
|
|
143
151
|
directives.redactionStrategy = r.directive;
|
|
144
152
|
break;
|
|
145
153
|
}
|
|
154
|
+
case 'step_up_scope': {
|
|
155
|
+
const r = buildStepUpScope(def, params, value);
|
|
156
|
+
if (r.error)
|
|
157
|
+
errors.push(r.error);
|
|
158
|
+
else
|
|
159
|
+
directives.stepUpScope = r.directive;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'step_up_four_eyes': {
|
|
163
|
+
const r = buildStepUpFourEyes(def, params, value);
|
|
164
|
+
if (r.error)
|
|
165
|
+
errors.push(r.error);
|
|
166
|
+
else
|
|
167
|
+
directives.stepUpFourEyes = r.directive;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'step_up_unless_granted': {
|
|
171
|
+
const r = buildStepUpUnlessGranted(def, params, value);
|
|
172
|
+
if (r.error)
|
|
173
|
+
errors.push(r.error);
|
|
174
|
+
else
|
|
175
|
+
directives.stepUpUnlessGranted = r.directive;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'single_use': {
|
|
179
|
+
const r = buildSingleUse(def, params, value);
|
|
180
|
+
if (r.error)
|
|
181
|
+
errors.push(r.error);
|
|
182
|
+
else
|
|
183
|
+
directives.singleUse = r.directive;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
146
186
|
default:
|
|
147
187
|
// Registry entry exists but no typed extractor is wired here — a
|
|
148
188
|
// programming error in highflame-policy. Fail closed at runtime.
|
|
@@ -154,12 +194,64 @@ export function parseAARMAnnotations(raw) {
|
|
|
154
194
|
}));
|
|
155
195
|
}
|
|
156
196
|
}
|
|
197
|
+
// The step-up modifiers only mean something next to @step_up_required.
|
|
198
|
+
// Without it the policy would permit outright while its author believes a
|
|
199
|
+
// human approval is required, so reject rather than ignore.
|
|
200
|
+
if (!directives.stepUpRequired) {
|
|
201
|
+
for (const key of ['step_up_four_eyes', 'step_up_scope', 'step_up_unless_granted']) {
|
|
202
|
+
if (key in raw && !errors.some((e) => e.key === key)) {
|
|
203
|
+
errors.push(new AARMAnnotationError({
|
|
204
|
+
key,
|
|
205
|
+
rawValue: raw[key] ?? '',
|
|
206
|
+
reason: 'requires @step_up_required on the same policy',
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// "self" routes the approval to the requesting identity's owner, and
|
|
212
|
+
// four-eyes forbids exactly that person from approving: every gated call
|
|
213
|
+
// would wait for an approval that cannot happen, then deny.
|
|
214
|
+
if (directives.stepUpRequired?.role === 'self' && directives.stepUpFourEyes) {
|
|
215
|
+
errors.push(new AARMAnnotationError({
|
|
216
|
+
key: 'step_up_four_eyes',
|
|
217
|
+
rawValue: raw.step_up_four_eyes ?? '',
|
|
218
|
+
reason: 'cannot be combined with @step_up_required("self"): self sends the approval ' +
|
|
219
|
+
'to the owner, whom four-eyes forbids from approving',
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
157
222
|
return { directives, errors };
|
|
158
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* THE authoring-time AARM check: run it on every rule of a policy before
|
|
226
|
+
* saving it (Studio's lint, Admin's policy-create/update), passing the rule's
|
|
227
|
+
* effect. It is validateAARMAnnotations plus the checks that depend on the
|
|
228
|
+
* effect: `@single_use` is only meaningful on a permit (a forbid never
|
|
229
|
+
* produces an allow to consume). validateAARMAnnotations alone is
|
|
230
|
+
* effect-blind and is not sufficient.
|
|
231
|
+
*
|
|
232
|
+
* Shield runs the same check at sync, but there an error is not always a
|
|
233
|
+
* reject: a permit with an invalid annotation is dropped (fail closed), while
|
|
234
|
+
* a forbid is loaded with the offending annotation ignored, since dropping a
|
|
235
|
+
* forbid would remove a block. Surface the error here, at save time.
|
|
236
|
+
*
|
|
237
|
+
* Exported from both the package root and the browser-safe `./types` entry.
|
|
238
|
+
*/
|
|
239
|
+
export function validateAARMAnnotationsForEffect(effect, raw) {
|
|
240
|
+
const errors = validateAARMAnnotations(raw);
|
|
241
|
+
if (raw && 'single_use' in raw && effect !== 'permit' && !errors.some((e) => e.key === 'single_use')) {
|
|
242
|
+
errors.push(new AARMAnnotationError({
|
|
243
|
+
key: 'single_use',
|
|
244
|
+
rawValue: raw.single_use ?? '',
|
|
245
|
+
reason: 'is only valid on a permit policy',
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
return errors;
|
|
249
|
+
}
|
|
159
250
|
/**
|
|
160
251
|
* Validate-only variant: returns the error list without the typed
|
|
161
|
-
* directives.
|
|
162
|
-
*
|
|
252
|
+
* directives. Effect-blind: authoring checks that know the rule's effect
|
|
253
|
+
* (Admin's policy-create, Studio's lint) must use
|
|
254
|
+
* validateAARMAnnotationsForEffect instead.
|
|
163
255
|
*/
|
|
164
256
|
export function validateAARMAnnotations(raw) {
|
|
165
257
|
return parseAARMAnnotations(raw).errors;
|
|
@@ -258,6 +350,16 @@ function parseAARMAnnotationParams(def, raw) {
|
|
|
258
350
|
// Single-positional shorthand. Bind the whole value to the positional param.
|
|
259
351
|
const pos = positionalParameter(def);
|
|
260
352
|
if (!pos) {
|
|
353
|
+
// An annotation whose only parameter is optional accepts both the bare
|
|
354
|
+
// (value-less) form, meaning "use the default", and a bare value as
|
|
355
|
+
// shorthand for that parameter: @single_use, @single_use("request").
|
|
356
|
+
const sole = soleOptionalParameter(def);
|
|
357
|
+
if (sole) {
|
|
358
|
+
const v = raw.trim();
|
|
359
|
+
if (v !== '')
|
|
360
|
+
values[sole.name] = v;
|
|
361
|
+
return { params: values };
|
|
362
|
+
}
|
|
261
363
|
return {
|
|
262
364
|
params: {},
|
|
263
365
|
error: new AARMAnnotationError({
|
|
@@ -321,6 +423,72 @@ function buildRedactionStrategy(def, params, raw) {
|
|
|
321
423
|
}
|
|
322
424
|
return { directive: { strategy: strategy.value } };
|
|
323
425
|
}
|
|
426
|
+
/** Mirrors the enums in schemas/annotations.json (the generated registry carries none). */
|
|
427
|
+
const STEP_UP_SCOPES = ['session'];
|
|
428
|
+
const SINGLE_USE_GRANULARITIES = ['tool', 'request'];
|
|
429
|
+
function buildStepUpScope(def, params, raw) {
|
|
430
|
+
const scope = requireStringParam(def, params, 'scope', raw);
|
|
431
|
+
if ('error' in scope)
|
|
432
|
+
return { error: scope.error };
|
|
433
|
+
if (!STEP_UP_SCOPES.includes(scope.value)) {
|
|
434
|
+
return {
|
|
435
|
+
error: new AARMAnnotationError({
|
|
436
|
+
key: def.key,
|
|
437
|
+
parameter: 'scope',
|
|
438
|
+
rawValue: raw,
|
|
439
|
+
reason: 'scope must be "session"; omit the annotation for the one-call default',
|
|
440
|
+
}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return { directive: { scope: scope.value } };
|
|
444
|
+
}
|
|
445
|
+
function buildStepUpFourEyes(def, params, raw) {
|
|
446
|
+
// Bare or "true" only: "false" would make the annotation present yet
|
|
447
|
+
// disabled, which consumers checking presence would misread.
|
|
448
|
+
const v = params.enabled;
|
|
449
|
+
if (v !== undefined && v !== '' && v !== 'true') {
|
|
450
|
+
return {
|
|
451
|
+
error: new AARMAnnotationError({
|
|
452
|
+
key: def.key,
|
|
453
|
+
parameter: 'enabled',
|
|
454
|
+
rawValue: raw,
|
|
455
|
+
reason: 'value must be omitted or "true"',
|
|
456
|
+
}),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return { directive: {} };
|
|
460
|
+
}
|
|
461
|
+
function buildStepUpUnlessGranted(def, params, raw) {
|
|
462
|
+
// Bare or "true" only, as for four-eyes: "false" would read as present
|
|
463
|
+
// (grant waives the approval) to a consumer checking presence.
|
|
464
|
+
const v = params.enabled;
|
|
465
|
+
if (v !== undefined && v !== '' && v !== 'true') {
|
|
466
|
+
return {
|
|
467
|
+
error: new AARMAnnotationError({
|
|
468
|
+
key: def.key,
|
|
469
|
+
parameter: 'enabled',
|
|
470
|
+
rawValue: raw,
|
|
471
|
+
reason: 'value must be omitted or "true"',
|
|
472
|
+
}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return { directive: {} };
|
|
476
|
+
}
|
|
477
|
+
function buildSingleUse(def, params, raw) {
|
|
478
|
+
const gDef = mustLookupParam(def, 'granularity');
|
|
479
|
+
const granularity = params.granularity || (gDef.default?.kind === 'string' ? gDef.default.value : 'tool');
|
|
480
|
+
if (!SINGLE_USE_GRANULARITIES.includes(granularity)) {
|
|
481
|
+
return {
|
|
482
|
+
error: new AARMAnnotationError({
|
|
483
|
+
key: def.key,
|
|
484
|
+
parameter: 'granularity',
|
|
485
|
+
rawValue: raw,
|
|
486
|
+
reason: 'value must be "tool" or "request" (or omitted for "tool"); values are case-sensitive',
|
|
487
|
+
}),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return { directive: { granularity } };
|
|
491
|
+
}
|
|
324
492
|
function buildDeferUntilContext(def, params, raw) {
|
|
325
493
|
const field = requireStringParam(def, params, 'field', raw);
|
|
326
494
|
if ('error' in field)
|
|
@@ -510,6 +678,11 @@ function hasParameter(def, name) {
|
|
|
510
678
|
function parameterNames(def) {
|
|
511
679
|
return def.parameters.map((p) => p.name);
|
|
512
680
|
}
|
|
681
|
+
/** The annotation's only parameter when it is optional and non-positional. */
|
|
682
|
+
function soleOptionalParameter(def) {
|
|
683
|
+
const [only] = def.parameters;
|
|
684
|
+
return def.parameters.length === 1 && !only.required && !only.positional ? only : undefined;
|
|
685
|
+
}
|
|
513
686
|
function positionalParameter(def) {
|
|
514
687
|
return def.parameters.find((p) => p.positional);
|
|
515
688
|
}
|
|
@@ -85,9 +85,51 @@ export const AARM_ANNOTATIONS = [
|
|
|
85
85
|
},
|
|
86
86
|
],
|
|
87
87
|
},
|
|
88
|
+
{
|
|
89
|
+
key: 'single_use',
|
|
90
|
+
description: 'Make an allow by this permit spendable exactly once. Shield records a consume key on the first allow and denies replays of the same grant. Bare means per tool; @single_use(\"tool\") and @single_use(\"request\") are explicit. Valid only on permit. A request without a bearer token (gateway or internal-service traffic) has no token id to record against and is denied single_use_unbound.',
|
|
91
|
+
aarmRequirement: 'R9',
|
|
92
|
+
promotesCapability: '',
|
|
93
|
+
decisionEffect: '',
|
|
94
|
+
parameters: [
|
|
95
|
+
{
|
|
96
|
+
name: 'granularity',
|
|
97
|
+
type: 'string',
|
|
98
|
+
required: false,
|
|
99
|
+
positional: false,
|
|
100
|
+
description: 'What one use covers. tool: any call to the same action and tool. request: only a call with the same canonicalized arguments. Case-sensitive.',
|
|
101
|
+
default: { kind: 'string', value: 'tool' },
|
|
102
|
+
min: null,
|
|
103
|
+
max: null,
|
|
104
|
+
pattern: '',
|
|
105
|
+
valueSource: '',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: 'step_up_four_eyes',
|
|
111
|
+
description: 'Require that the approver is a different person from the requesting agent\'s owner. Bare (value-less) or @step_up_four_eyes(\"true\"); no other value is legal. Requires @step_up_required on the same policy.',
|
|
112
|
+
aarmRequirement: 'R4',
|
|
113
|
+
promotesCapability: '',
|
|
114
|
+
decisionEffect: '',
|
|
115
|
+
parameters: [
|
|
116
|
+
{
|
|
117
|
+
name: 'enabled',
|
|
118
|
+
type: 'bool',
|
|
119
|
+
required: false,
|
|
120
|
+
positional: false,
|
|
121
|
+
description: 'Always true when present. Accepted spellings are the bare annotation and the string \"true\"; \"false\" is rejected so the annotation cannot be present yet disabled.',
|
|
122
|
+
default: { kind: 'bool', value: true },
|
|
123
|
+
min: null,
|
|
124
|
+
max: null,
|
|
125
|
+
pattern: '',
|
|
126
|
+
valueSource: '',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
88
130
|
{
|
|
89
131
|
key: 'step_up_required',
|
|
90
|
-
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).',
|
|
132
|
+
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). One approver role per matching call: when two or more enforce-mode step-up policies of the same effect match one call and name different roles (\"self\" excepted, since any named role outranks owner approval), no single approval satisfies them, so Shield denies the call with step_up_internal_error. Policy authoring lints this as a warning (step_up_role_conflict).',
|
|
91
133
|
aarmRequirement: 'R4',
|
|
92
134
|
promotesCapability: 'CAP-ENF-004',
|
|
93
135
|
decisionEffect: 'step_up',
|
|
@@ -118,6 +160,48 @@ export const AARM_ANNOTATIONS = [
|
|
|
118
160
|
},
|
|
119
161
|
],
|
|
120
162
|
},
|
|
163
|
+
{
|
|
164
|
+
key: 'step_up_scope',
|
|
165
|
+
description: 'Widen a step-up approval from one call to the approved tool for the life of the approval token. Without this annotation an approval authorizes exactly one call. With scope session, repeated calls to the SAME tool are covered until the token expires; other tools are never covered. Requires @step_up_required on the same policy.',
|
|
166
|
+
aarmRequirement: 'R4',
|
|
167
|
+
promotesCapability: '',
|
|
168
|
+
decisionEffect: '',
|
|
169
|
+
parameters: [
|
|
170
|
+
{
|
|
171
|
+
name: 'scope',
|
|
172
|
+
type: 'string',
|
|
173
|
+
required: true,
|
|
174
|
+
positional: true,
|
|
175
|
+
description: 'Approval scope. The only legal value is \"session\"; omit the annotation for the one-call default. Case-sensitive.',
|
|
176
|
+
default: null,
|
|
177
|
+
min: null,
|
|
178
|
+
max: null,
|
|
179
|
+
pattern: '',
|
|
180
|
+
valueSource: '',
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: 'step_up_unless_granted',
|
|
186
|
+
description: 'Let an explicit agent grant satisfy this policy\'s step-up approval. When the call is explicitly allowed by an enforcing allow grant on the agent\'s access table, Shield waives the challenge; otherwise the approval is required as usual. If another step-up policy matching the same call lacks this annotation, the challenge is still issued. Restrictions (deny grants) always deny. Bare (value-less) or @step_up_unless_granted(\"true\"); no other value is legal. Requires @step_up_required on the same policy.',
|
|
187
|
+
aarmRequirement: 'R4',
|
|
188
|
+
promotesCapability: '',
|
|
189
|
+
decisionEffect: '',
|
|
190
|
+
parameters: [
|
|
191
|
+
{
|
|
192
|
+
name: 'enabled',
|
|
193
|
+
type: 'bool',
|
|
194
|
+
required: false,
|
|
195
|
+
positional: false,
|
|
196
|
+
description: 'Always true when present. Accepted spellings are the bare annotation and the string \"true\"; \"false\" is rejected so the annotation cannot be present yet disabled.',
|
|
197
|
+
default: { kind: 'bool', value: true },
|
|
198
|
+
min: null,
|
|
199
|
+
max: null,
|
|
200
|
+
pattern: '',
|
|
201
|
+
valueSource: '',
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
121
205
|
];
|
|
122
206
|
/**
|
|
123
207
|
* O(1) lookup over AARM_ANNOTATIONS. Use this when validating raw
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AiGateway policy category identifiers.
|
|
3
3
|
* Maps to UI tab names in Studio.
|
|
4
4
|
*/
|
|
5
|
-
export type AiGatewayCategory = 'semantic' | 'tools' | 'agent-security' | 'data-protection' | 'privacy' | 'organization';
|
|
5
|
+
export type AiGatewayCategory = 'semantic' | 'tools' | 'agent-security' | 'data-protection' | 'privacy' | 'agent-identity' | 'organization';
|
|
6
6
|
/**
|
|
7
7
|
* Category metadata for UI display.
|
|
8
8
|
*/
|