@highflame/policy 2.2.44 → 2.3.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/defaults/semantic.cedar +30 -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/single_use_sensitive_tools.cedar +54 -0
- package/_schemas/guardrails/templates/templates.json +31 -0
- package/_schemas/guardrails/templates/unadopted_agents.cedar +64 -0
- package/dist/aarm-annotation.d.ts +48 -4
- package/dist/aarm-annotation.js +152 -4
- package/dist/aarm-annotations.gen.js +63 -0
- package/dist/ai_gateway-defaults.gen.d.ts +1 -1
- package/dist/ai_gateway-defaults.gen.js +206 -0
- package/dist/annotations.d.ts +3 -2
- package/dist/annotations.js +6 -2
- package/dist/guardrails-defaults.gen.js +169 -0
- package/dist/service-schemas.gen.d.ts +3 -3
- package/dist/service-schemas.gen.js +92 -0
- package/package.json +1 -1
|
@@ -118,6 +118,18 @@ namespace AgentOps {
|
|
|
118
118
|
context: ProvisionSandboxContext
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
+
/// The acting agent's lifecycle and IdP provenance, projected as
|
|
122
|
+
/// `context.actor`. Same four attributes, names and types as the legacy
|
|
123
|
+
/// members on PrincipalContext (which keeps them). Every member is optional
|
|
124
|
+
/// and the record is absent when none is supplied; absence means unknown,
|
|
125
|
+
/// not "active".
|
|
126
|
+
type ActorContext = {
|
|
127
|
+
"status"?: String, // Agent lifecycle: "discovered" | "pending" | "active"
|
|
128
|
+
"origin"?: String, // "native" | "entra" | "okta" | ...
|
|
129
|
+
"idp_iss"?: String, // Upstream IdP issuer for IdP-registered agents
|
|
130
|
+
"idp_parent"?: String, // Upstream parent object (e.g. Entra blueprint app id)
|
|
131
|
+
};
|
|
132
|
+
|
|
121
133
|
// =========================================================================
|
|
122
134
|
// Context Types (Action-Specific)
|
|
123
135
|
// =========================================================================
|
|
@@ -158,6 +170,10 @@ namespace AgentOps {
|
|
|
158
170
|
"act_iss"?: String, // Issuer of the actor claim
|
|
159
171
|
"mission_id"?: String, // Correlates one delegation tree
|
|
160
172
|
"grant_type"?: String, // How the token was obtained (e.g. "api_key")
|
|
173
|
+
"status"?: String, // Agent lifecycle: "discovered" | "pending" | "active"; absent = unknown, not active
|
|
174
|
+
"origin"?: String, // Where the identity came from: "native" | "entra" | "okta" | ...
|
|
175
|
+
"idp_iss"?: String, // Upstream IdP issuer for IdP-registered agents
|
|
176
|
+
"idp_parent"?: String, // Upstream parent object (e.g. Entra blueprint app id)
|
|
161
177
|
};
|
|
162
178
|
|
|
163
179
|
/// Context for the process_response action.
|
|
@@ -175,6 +191,7 @@ namespace AgentOps {
|
|
|
175
191
|
"privilege_scope"?: Set<String>,
|
|
176
192
|
"identity_type"?: String, // Principal identity class: "human" | "agent" | "service" | "mcp_server"
|
|
177
193
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
194
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
178
195
|
// Core metadata (required)
|
|
179
196
|
"request_id": String,
|
|
180
197
|
"timestamp": Long,
|
|
@@ -370,6 +387,7 @@ namespace AgentOps {
|
|
|
370
387
|
"privilege_scope"?: Set<String>,
|
|
371
388
|
"identity_type"?: String,
|
|
372
389
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
390
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
373
391
|
// Core metadata (required)
|
|
374
392
|
"request_id": String,
|
|
375
393
|
"timestamp": Long,
|
|
@@ -581,6 +599,7 @@ namespace AgentOps {
|
|
|
581
599
|
"privilege_scope"?: Set<String>,
|
|
582
600
|
"identity_type"?: String,
|
|
583
601
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
602
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
584
603
|
// Core metadata (required)
|
|
585
604
|
"request_id": String,
|
|
586
605
|
"timestamp": Long,
|
|
@@ -689,6 +708,7 @@ namespace AgentOps {
|
|
|
689
708
|
"privilege_scope"?: Set<String>,
|
|
690
709
|
"identity_type"?: String,
|
|
691
710
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
711
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
692
712
|
// Core metadata (required)
|
|
693
713
|
"request_id": String,
|
|
694
714
|
"timestamp": Long,
|
|
@@ -801,6 +821,7 @@ namespace AgentOps {
|
|
|
801
821
|
"privilege_scope"?: Set<String>,
|
|
802
822
|
"identity_type"?: String,
|
|
803
823
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
824
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
804
825
|
// Core metadata (required)
|
|
805
826
|
"request_id": String,
|
|
806
827
|
"timestamp": Long,
|
|
@@ -934,6 +955,7 @@ namespace AgentOps {
|
|
|
934
955
|
"privilege_scope"?: Set<String>,
|
|
935
956
|
"identity_type"?: String, // Principal identity class: "human" | "agent" | "service" | "mcp_server"
|
|
936
957
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
958
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
937
959
|
"agent_trust_level"?: String, // "untrusted" | "low" | "verified" | "trusted"
|
|
938
960
|
|
|
939
961
|
// Core metadata (required)
|
|
@@ -83,6 +83,8 @@ action call_tool appliesTo {
|
|
|
83
83
|
// Identity (AARM R6 / CAP-IDN-011) — projected from the principal's token; optional.
|
|
84
84
|
role?: String,
|
|
85
85
|
privilege_scope?: Set<String>,
|
|
86
|
+
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
87
|
+
"actor"?: ActorContext, // Acting agent status/origin/IdP provenance; absent when none supplied
|
|
86
88
|
// --- Content ---
|
|
87
89
|
content: String, // Raw content being scanned
|
|
88
90
|
|
|
@@ -205,6 +207,8 @@ action connect_server appliesTo {
|
|
|
205
207
|
// Identity (AARM R6 / CAP-IDN-011) — projected from the principal's token; optional.
|
|
206
208
|
role?: String,
|
|
207
209
|
privilege_scope?: Set<String>,
|
|
210
|
+
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
211
|
+
"actor"?: ActorContext, // Acting agent status/origin/IdP provenance; absent when none supplied
|
|
208
212
|
content?: String, // Server config content (if available)
|
|
209
213
|
mcp_server?: String,
|
|
210
214
|
|
|
@@ -302,6 +306,8 @@ action list appliesTo {
|
|
|
302
306
|
// Identity (AARM R6 / CAP-IDN-011) — projected from the principal's token; optional.
|
|
303
307
|
role?: String,
|
|
304
308
|
privilege_scope?: Set<String>,
|
|
309
|
+
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
310
|
+
"actor"?: ActorContext, // Acting agent status/origin/IdP provenance; absent when none supplied
|
|
305
311
|
|
|
306
312
|
// --- Content ---
|
|
307
313
|
// The enumerated item's description. Optional: a tool may declare none,
|
|
@@ -376,6 +382,43 @@ action list appliesTo {
|
|
|
376
382
|
},
|
|
377
383
|
};
|
|
378
384
|
|
|
385
|
+
// ZeroID NHI principal block, projected by Shield as `context.principal` when
|
|
386
|
+
// the caller's token carries NHI claims. Same shape as the Guardrails and
|
|
387
|
+
// AgentOps PrincipalContext, so one policy text reads the same keys on every
|
|
388
|
+
// product. Every member is optional except `delegation_depth` (always emitted;
|
|
389
|
+
// 0 for a direct token). The record is absent, not empty, when the request
|
|
390
|
+
// carries no NHI claims, so `context has principal` is false.
|
|
391
|
+
type PrincipalContext = {
|
|
392
|
+
"identity_type"?: String, // "human" | "agent" | "service" | "mcp_server"
|
|
393
|
+
"sub_type"?: String, // Identity subtype (e.g. "coding_agent")
|
|
394
|
+
"trust_level"?: String, // "first_party" | "verified" | "unverified"
|
|
395
|
+
"framework"?: String, // Agent framework (e.g. "langchain")
|
|
396
|
+
"publisher"?: String, // Agent publisher
|
|
397
|
+
"capabilities"?: Set<String>, // Capabilities granted to the identity
|
|
398
|
+
"scopes"?: Set<String>, // OAuth scopes carried by the token
|
|
399
|
+
"delegation_depth": Long, // Delegation hops; 0 for a direct token
|
|
400
|
+
"act_sub"?: String, // RFC 8693 actor: the accountable human
|
|
401
|
+
"act_iss"?: String, // Issuer of the actor claim
|
|
402
|
+
"mission_id"?: String, // Correlates one delegation tree
|
|
403
|
+
"grant_type"?: String, // How the token was obtained (e.g. "api_key")
|
|
404
|
+
"status"?: String, // Agent lifecycle: "discovered" | "pending" | "active"; absent = unknown, not active
|
|
405
|
+
"origin"?: String, // Where the identity came from: "native" | "entra" | "okta" | ...
|
|
406
|
+
"idp_iss"?: String, // Upstream IdP issuer for IdP-registered agents
|
|
407
|
+
"idp_parent"?: String, // Upstream parent object (e.g. Entra blueprint app id)
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// The acting agent's lifecycle and IdP provenance, projected as
|
|
411
|
+
// `context.actor`. Firehog traffic is evaluated in this namespace, which is
|
|
412
|
+
// where agents registered in Okta or Entra reach Shield (ADR 0041 D5,
|
|
413
|
+
// ADR 0042). Every member is optional and the record is absent when none is
|
|
414
|
+
// supplied; absence means unknown, not "active".
|
|
415
|
+
type ActorContext = {
|
|
416
|
+
"status"?: String, // Agent lifecycle: "discovered" | "pending" | "active"
|
|
417
|
+
"origin"?: String, // "native" | "entra" | "okta" | ...
|
|
418
|
+
"idp_iss"?: String, // Upstream IdP issuer for IdP-registered agents
|
|
419
|
+
"idp_parent"?: String, // Upstream parent object (e.g. Entra blueprint app id)
|
|
420
|
+
};
|
|
421
|
+
|
|
379
422
|
// Shared content-inspection context.
|
|
380
423
|
//
|
|
381
424
|
// Prompts and responses are inspected by the same detector pipeline and so
|
|
@@ -386,6 +429,8 @@ type ContentInspectionContext = {
|
|
|
386
429
|
// Identity (AARM R6 / CAP-IDN-011) — projected from the principal's token; optional.
|
|
387
430
|
role?: String,
|
|
388
431
|
privilege_scope?: Set<String>,
|
|
432
|
+
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
433
|
+
"actor"?: ActorContext, // Acting agent status/origin/IdP provenance; absent when none supplied
|
|
389
434
|
content: String,
|
|
390
435
|
mcp_server?: String,
|
|
391
436
|
|
|
@@ -497,6 +542,8 @@ action read_file appliesTo {
|
|
|
497
542
|
// Identity (AARM R6 / CAP-IDN-011) — projected from the principal's token; optional.
|
|
498
543
|
role?: String,
|
|
499
544
|
privilege_scope?: Set<String>,
|
|
545
|
+
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
546
|
+
"actor"?: ActorContext, // Acting agent status/origin/IdP provenance; absent when none supplied
|
|
500
547
|
content: String,
|
|
501
548
|
mcp_server?: String,
|
|
502
549
|
|
|
@@ -564,6 +611,8 @@ action write_file appliesTo {
|
|
|
564
611
|
// Identity (AARM R6 / CAP-IDN-011) — projected from the principal's token; optional.
|
|
565
612
|
role?: String,
|
|
566
613
|
privilege_scope?: Set<String>,
|
|
614
|
+
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
615
|
+
"actor"?: ActorContext, // Acting agent status/origin/IdP provenance; absent when none supplied
|
|
567
616
|
content: String,
|
|
568
617
|
mcp_server?: String,
|
|
569
618
|
|
|
@@ -28,6 +28,21 @@
|
|
|
28
28
|
// Section 1: Prompt injection
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
|
|
31
|
+
@id("semantic.block-injection-prompt-ml")
|
|
32
|
+
@name("Block prompt injection in LLM prompts (ML)")
|
|
33
|
+
@description("Blocks process_prompt when injection_score >= 75.")
|
|
34
|
+
@severity("critical")
|
|
35
|
+
@tags("category:semantic,threat:injection,detection:ml,surface:process-prompt,owasp:llm01,mitre:atlas-t0051")
|
|
36
|
+
@reject_message("Prompt blocked: the ML classifier detected prompt injection with high confidence.")
|
|
37
|
+
forbid (
|
|
38
|
+
principal,
|
|
39
|
+
action == AIGateway::Action::"process_prompt",
|
|
40
|
+
resource
|
|
41
|
+
)
|
|
42
|
+
when {
|
|
43
|
+
context has injection_score && context.injection_score >= 75
|
|
44
|
+
};
|
|
45
|
+
|
|
31
46
|
@id("semantic.block-injection-ml")
|
|
32
47
|
@name("Block injection (ML)")
|
|
33
48
|
@description("Blocks call_tool when injection_score >= 75.")
|
|
@@ -47,6 +62,21 @@ when {
|
|
|
47
62
|
// Section 2: Jailbreak
|
|
48
63
|
// ---------------------------------------------------------------------------
|
|
49
64
|
|
|
65
|
+
@id("semantic.block-jailbreak-prompt-ml")
|
|
66
|
+
@name("Block jailbreak in LLM prompts (ML)")
|
|
67
|
+
@description("Blocks process_prompt when jailbreak_score >= 75.")
|
|
68
|
+
@severity("critical")
|
|
69
|
+
@tags("category:semantic,threat:jailbreak,detection:ml,surface:process-prompt,owasp:llm02,mitre:atlas-t0054")
|
|
70
|
+
@reject_message("Prompt blocked: the ML classifier detected a jailbreak attempt with high confidence.")
|
|
71
|
+
forbid (
|
|
72
|
+
principal,
|
|
73
|
+
action == AIGateway::Action::"process_prompt",
|
|
74
|
+
resource
|
|
75
|
+
)
|
|
76
|
+
when {
|
|
77
|
+
context has jailbreak_score && context.jailbreak_score >= 75
|
|
78
|
+
};
|
|
79
|
+
|
|
50
80
|
@id("semantic.block-jailbreak-ml")
|
|
51
81
|
@name("Block jailbreak (ML)")
|
|
52
82
|
@description("Blocks call_tool when jailbreak_score >= 75.")
|
|
@@ -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: AIGateway
|
|
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 == AIGateway::Action::"call_tool",
|
|
50
|
+
resource
|
|
51
|
+
)
|
|
52
|
+
when {
|
|
53
|
+
context has tool_is_sensitive && context.tool_is_sensitive
|
|
54
|
+
};
|
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
"description": "Block personally identifiable information (PII) in LLM prompts and MCP tool calls.",
|
|
34
34
|
"layer": "guardrail"
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
"id": "agent-identity",
|
|
38
|
+
"name": "Agent Identity",
|
|
39
|
+
"description": "Agent lifecycle and provenance controls for gateway traffic: unadopted agents, single-use sensitive tools.",
|
|
40
|
+
"layer": "guardrail"
|
|
41
|
+
},
|
|
36
42
|
{
|
|
37
43
|
"id": "organization",
|
|
38
44
|
"name": "Organization",
|
|
@@ -197,6 +203,37 @@
|
|
|
197
203
|
"compliance:pci-dss",
|
|
198
204
|
"compliance:hipaa"
|
|
199
205
|
]
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"id": "agent-identity.block-unadopted-agent-sensitive-tools",
|
|
209
|
+
"name": "Unadopted Agents: No Sensitive Tool Calls",
|
|
210
|
+
"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.",
|
|
211
|
+
"category": "agent-identity",
|
|
212
|
+
"file": "unadopted_agents.cedar",
|
|
213
|
+
"severity": "high",
|
|
214
|
+
"tags": [
|
|
215
|
+
"category:agent-identity",
|
|
216
|
+
"detection:rule",
|
|
217
|
+
"surface:call-tool",
|
|
218
|
+
"scope:per-agent",
|
|
219
|
+
"posture:deny-default",
|
|
220
|
+
"owasp:asi04"
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"id": "agent-identity.permit-single-use-sensitive-tools",
|
|
225
|
+
"name": "Single-Use for Sensitive Tools",
|
|
226
|
+
"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.",
|
|
227
|
+
"category": "agent-identity",
|
|
228
|
+
"file": "single_use_sensitive_tools.cedar",
|
|
229
|
+
"severity": "medium",
|
|
230
|
+
"tags": [
|
|
231
|
+
"category:agent-identity",
|
|
232
|
+
"surface:call-tool",
|
|
233
|
+
"scope:per-tool",
|
|
234
|
+
"owasp:asi02",
|
|
235
|
+
"single-use"
|
|
236
|
+
]
|
|
200
237
|
}
|
|
201
238
|
]
|
|
202
239
|
}
|
|
@@ -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 gateway traffic (Firehog), which Shield evaluates in the
|
|
30
|
+
// AIGateway namespace. The Guardrails copy covers Guardrails traffic.
|
|
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: AIGateway
|
|
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 == AIGateway::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
|
+
};
|
|
@@ -141,6 +141,22 @@ namespace Guardrails {
|
|
|
141
141
|
"act_iss"?: String, // Issuer of the actor claim
|
|
142
142
|
"mission_id"?: String, // Correlates one delegation tree
|
|
143
143
|
"grant_type"?: String, // How the token was obtained (e.g. "api_key")
|
|
144
|
+
"status"?: String, // Agent lifecycle: "discovered" | "pending" | "active"; absent = unknown, not active
|
|
145
|
+
"origin"?: String, // Where the identity came from: "native" | "entra" | "okta" | ...
|
|
146
|
+
"idp_iss"?: String, // Upstream IdP issuer for IdP-registered agents
|
|
147
|
+
"idp_parent"?: String, // Upstream parent object (e.g. Entra blueprint app id)
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/// The acting agent's lifecycle and IdP provenance, projected as
|
|
151
|
+
/// `context.actor`. Same four attributes, names and types as the legacy
|
|
152
|
+
/// members on PrincipalContext (which keeps them). Every member is optional
|
|
153
|
+
/// and the record is absent when none is supplied; absence means unknown,
|
|
154
|
+
/// not "active".
|
|
155
|
+
type ActorContext = {
|
|
156
|
+
"status"?: String, // Agent lifecycle: "discovered" | "pending" | "active"
|
|
157
|
+
"origin"?: String, // "native" | "entra" | "okta" | ...
|
|
158
|
+
"idp_iss"?: String, // Upstream IdP issuer for IdP-registered agents
|
|
159
|
+
"idp_parent"?: String, // Upstream parent object (e.g. Entra blueprint app id)
|
|
144
160
|
};
|
|
145
161
|
|
|
146
162
|
/// Context for the process_response action.
|
|
@@ -160,6 +176,7 @@ namespace Guardrails {
|
|
|
160
176
|
"privilege_scope"?: Set<String>,
|
|
161
177
|
"identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
|
|
162
178
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
179
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
163
180
|
// Core metadata (required)
|
|
164
181
|
"request_id": String,
|
|
165
182
|
"timestamp": Long,
|
|
@@ -312,6 +329,7 @@ namespace Guardrails {
|
|
|
312
329
|
"privilege_scope"?: Set<String>,
|
|
313
330
|
"identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
|
|
314
331
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
332
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
315
333
|
// Core metadata (required)
|
|
316
334
|
"request_id": String,
|
|
317
335
|
"timestamp": Long,
|
|
@@ -508,6 +526,7 @@ namespace Guardrails {
|
|
|
508
526
|
"privilege_scope"?: Set<String>,
|
|
509
527
|
"identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
|
|
510
528
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
529
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
511
530
|
// Core metadata (required)
|
|
512
531
|
"request_id": String,
|
|
513
532
|
"timestamp": Long,
|
|
@@ -581,6 +600,7 @@ namespace Guardrails {
|
|
|
581
600
|
"privilege_scope"?: Set<String>,
|
|
582
601
|
"identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
|
|
583
602
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
603
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
584
604
|
// Core metadata (required)
|
|
585
605
|
"request_id": String,
|
|
586
606
|
"timestamp": Long,
|
|
@@ -658,6 +678,7 @@ namespace Guardrails {
|
|
|
658
678
|
"privilege_scope"?: Set<String>,
|
|
659
679
|
"identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
|
|
660
680
|
"principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
|
|
681
|
+
"actor"?: ActorContext, // Acting agent attributes (status/origin/IdP provenance); absent when none supplied
|
|
661
682
|
// Core metadata (required)
|
|
662
683
|
"request_id": String,
|
|
663
684
|
"timestamp": Long,
|
|
@@ -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
|
+
};
|
|
@@ -649,6 +649,37 @@
|
|
|
649
649
|
"posture:deny-default",
|
|
650
650
|
"owasp:asi01"
|
|
651
651
|
]
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"id": "agent-identity.block-unadopted-agent-sensitive-tools",
|
|
655
|
+
"name": "Unadopted Agents: No Sensitive Tool Calls",
|
|
656
|
+
"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.",
|
|
657
|
+
"category": "agent-identity",
|
|
658
|
+
"file": "unadopted_agents.cedar",
|
|
659
|
+
"severity": "high",
|
|
660
|
+
"tags": [
|
|
661
|
+
"category:agent-identity",
|
|
662
|
+
"detection:rule",
|
|
663
|
+
"surface:call-tool",
|
|
664
|
+
"scope:per-agent",
|
|
665
|
+
"posture:deny-default",
|
|
666
|
+
"owasp:asi04"
|
|
667
|
+
]
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
"id": "agent-identity.permit-single-use-sensitive-tools",
|
|
671
|
+
"name": "Single-Use for Sensitive Tools",
|
|
672
|
+
"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.",
|
|
673
|
+
"category": "agent-identity",
|
|
674
|
+
"file": "single_use_sensitive_tools.cedar",
|
|
675
|
+
"severity": "medium",
|
|
676
|
+
"tags": [
|
|
677
|
+
"category:agent-identity",
|
|
678
|
+
"surface:call-tool",
|
|
679
|
+
"scope:per-tool",
|
|
680
|
+
"owasp:asi02",
|
|
681
|
+
"single-use"
|
|
682
|
+
]
|
|
652
683
|
}
|
|
653
684
|
]
|
|
654
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
|
+
};
|