@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.
@@ -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
+ };