@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.
@@ -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
 
@@ -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,
@@ -1,22 +1,77 @@
1
1
  // =============================================================================
2
2
  // Dual Attribution
3
3
  // =============================================================================
4
- // Blocks privileged agent actions that cannot be attributed to a human. "Which
5
- // agent did this?" is half an answer; the other half is "on whose behalf?" An
6
- // agent is not an accountable party — the person who pointed it at the work
7
- // is. Shield projects both sides of that pair, so the requirement can be a
8
- // policy rather than a reporting convention nobody enforces.
4
+ // Routes privileged agent actions that no human answers for to explicit
5
+ // human approval. "Which agent did this?" is half an answer; the other half
6
+ // is "on whose behalf?" When an agent acts for a person, that person is the
7
+ // accountable party. When it acts for nobody (an autonomous agent, a
8
+ // workload, an api_key-rooted token), its sensitive tool calls run only when
9
+ // someone explicitly authorizes them, never silently and never by default.
9
10
  //
10
11
  // The agent comes from the authenticated identity, never from the request
11
12
  // body: agent_id, agent_type, agent_trust_level, agent_framework. The human
12
- // comes from the identity claims on the credential, as the `principal` record
13
- // — its `act_sub` field names the party the agent is acting for.
13
+ // comes from the identity claims on the credential, as the `principal`
14
+ // record. Its `act_sub` field names the human the agent acts for, and is
15
+ // absent when there is none: legacy tokens carry the delegating parent (or
16
+ // the api key's creator), while rfc8693 tokens carry it only when the
17
+ // delegation chain is rooted in a user. Autonomous, workload and
18
+ // api_key-rooted rfc8693 tokens have no act_sub.
14
19
  //
15
- // Deploy in MONITOR mode first. Section 2 blocks unverified agents, and a
16
- // service key authenticates as unverified until the agent is registered and
17
- // adopted in Studio. Monitor records what each rule would have blocked on
18
- // every event without blocking anything; register the agent, then switch to
19
- // enforce.
20
+ // How an agent call with no acting human is authorized: it needs an
21
+ // explicit approval OR an explicit grant.
22
+ // 1. Approval. Section 1 permits the call but suspends it for step-up
23
+ // approval (@step_up_required, AARM R4, CAP-ENF-004): Shield issues an
24
+ // OpenID CIBA challenge and the call runs only after an approver with
25
+ // the named AuthN role approves it; the timeout denies it (fail-closed).
26
+ // 2. Grants. Allow/deny rows in Studio's agent access table compile to
27
+ // permits (grants) and forbids (restrictions) that Shield evaluates per
28
+ // agent. A restriction always denies. Section 1 carries
29
+ // @step_up_unless_granted, so when an allow grant explicitly matches
30
+ // the call under an ENFORCING access posture, Shield waives the
31
+ // challenge and the call runs (recorded as step_up_waived_by_grant).
32
+ // Under the default shadow posture a grant is only observed, so it
33
+ // waives nothing and approval decides. With no matching grant the call
34
+ // is challenged under either posture, not denied for lack of a grant.
35
+ // The waiver needs every step-up policy matching the call to carry
36
+ // @step_up_unless_granted: another step-up policy without it still
37
+ // challenges. Shield releases older than the grant waiver ignore the
38
+ // annotation: there a no-human call needs the approval even when granted,
39
+ // and under an enforcing posture an ungranted one is denied outright.
40
+ //
41
+ // A call carrying an acting human (act_sub present) is untouched by
42
+ // Section 1, unless the agent is autonomous and not first_party.
43
+ //
44
+ // Customize Section 1 before deploying:
45
+ // - @step_up_unless_granted: remove it to require the approval even for
46
+ // calls an allow grant covers (grant AND approval).
47
+ // - role: must exist in authn.roles ("security_oncall" is a placeholder;
48
+ // unknown roles reject the policy at deploy time). Another step-up
49
+ // policy matching the same call must name the same role, or Shield
50
+ // denies the call (one approval cannot satisfy two roles).
51
+ // - timeout_seconds: this template sets 3600 (1h); when omitted the
52
+ // default is 86400 (24h). Bounds [60, 604800].
53
+ //
54
+ // Deploy this policy in ENFORCE mode. Section 1's approval gate exists only
55
+ // in enforce mode: in monitor or alert mode Shield issues no challenge and
56
+ // passes Section 1's Allow through unchanged. Section 1 is a permit, so in
57
+ // a policy set without the baseline permit (organization.permit-baseline)
58
+ // it is what lifts the implicit deny for these calls. In enforce mode that
59
+ // lift is gated on approval (or a grant); in monitor mode the same calls
60
+ // would be allowed with no approval at all, a widening, not an observation.
61
+ //
62
+ // Only Section 2 is safe to observe first. It blocks unverified agents, and
63
+ // a service key authenticates as unverified until the agent is registered
64
+ // and adopted in Studio. Either register and adopt the agents before
65
+ // deploying, or move Section 2 into its own policy, deploy that one in
66
+ // MONITOR mode to see what it would block, and switch it to enforce once
67
+ // the agents are adopted. Mode is per policy, so never put this whole
68
+ // template in monitor to trial Section 2.
69
+ //
70
+ // Change note: Section 3 (autonomous agents must be first_party) used to be
71
+ // a hard block. It is now folded into Section 1, so a non-first-party
72
+ // autonomous agent's sensitive call is routed to approval (or a grant)
73
+ // instead of being denied. To restore the hard block, add the forbid kept
74
+ // in the Section 3 comment below; it overrides approvals and grants.
20
75
  //
21
76
  // Detection layers:
22
77
  // - agent identity (authentication layer, always available)
@@ -24,7 +79,8 @@
24
79
  //
25
80
  // Context keys consumed:
26
81
  // - agent_id, agent_type, agent_trust_level: String
27
- // - principal: record — act_sub names the accountable human
82
+ // - principal: record; act_sub names the acting human, absent when the
83
+ // agent acts for nobody
28
84
  // - tool_is_sensitive: Bool
29
85
  // - tool_category: String
30
86
  //
@@ -36,33 +92,69 @@
36
92
  // =============================================================================
37
93
 
38
94
  // ---------------------------------------------------------------------------
39
- // Section 1: No unattributed privileged action
40
- // `unless` because the rule must fire when attribution is ABSENT, and an
41
- // absent field cannot be compared — only tested for.
95
+ // Section 1: No-human privileged action requires approval
96
+ // ENFORCE mode only: in monitor mode this permit allows the call with no
97
+ // approval (see the header).
98
+ // Matches when attribution is ABSENT (an absent field cannot be compared,
99
+ // only tested for), and also for autonomous agents that are not first_party
100
+ // even when a human is attached: nobody watches an autonomous agent in real
101
+ // time, and a legacy api_key token names the key's creator as act_sub
102
+ // whether or not that person is directing the call.
103
+ //
104
+ // Cedar is deny-overrides and Shield issues step-up only when the decision
105
+ // is Allow, so this permit must not coexist with a forbid on the same calls:
106
+ // the forbid would deny every one of them and no approval would be asked.
107
+ // To hard-block instead of gating on approval, replace this rule with:
108
+ //
109
+ // @id("agent-identity.require-principal-for-sensitive-tools")
110
+ // @name("Block sensitive agent tools without a principal")
111
+ // @description("Blocks call_tool on a sensitive tool by an agent unless the credential carries a principal whose act_sub names the human the agent acts for.")
112
+ // @severity("critical")
113
+ // @tags("category:agent-identity,detection:rule,scope:per-agent,surface:call-tool,owasp:asi01,posture:deny-default")
114
+ // @reject_message("Tool execution blocked: this agent invoked a sensitive tool with no accountable human attached.")
115
+ // forbid (
116
+ // principal,
117
+ // action == Guardrails::Action::"call_tool",
118
+ // resource
119
+ // )
120
+ // when {
121
+ // context has agent_id && context.agent_id != "" &&
122
+ // context has tool_is_sensitive && context.tool_is_sensitive == true
123
+ // }
124
+ // unless {
125
+ // context has principal && context.principal has act_sub
126
+ // };
42
127
  // ---------------------------------------------------------------------------
43
128
 
44
- @id("agent-identity.require-principal-for-sensitive-tools")
45
- @name("Block sensitive agent tools without a principal")
46
- @description("Blocks call_tool on a sensitive tool by an agent unless the credential carries a principal whose act_sub names the human the agent acts for.")
129
+ @id("agent-identity.step-up-sensitive-tools-without-principal")
130
+ @name("Permit no-human sensitive agent tools with step-up approval")
131
+ @description("Permits call_tool on a sensitive tool by an agent with no acting human (no act_sub), or an autonomous agent that is not first_party, but suspends it until an approver with the configured AuthN role approves, unless an enforcing allow grant explicitly covers the call.")
47
132
  @severity("critical")
48
- @tags("category:agent-identity,detection:rule,surface:call-tool,scope:per-agent,posture:deny-default,owasp:asi01")
49
- @reject_message("Tool execution blocked: this agent invoked a sensitive tool with no accountable human attached. Every privileged agent action must name the person it acts for.")
50
- forbid (
133
+ @tags("category:agent-identity,detection:rule,scope:per-agent,surface:call-tool,posture:deny-default,owasp:asi01,step-up")
134
+ @step_up_required("role=security_oncall,timeout_seconds=3600")
135
+ @step_up_unless_granted
136
+ permit (
51
137
  principal,
52
138
  action == Guardrails::Action::"call_tool",
53
139
  resource
54
140
  )
55
141
  when {
56
142
  context has agent_id && context.agent_id != "" &&
57
- context has tool_is_sensitive && context.tool_is_sensitive == true
58
- }
59
- unless {
60
- context has principal && context.principal has act_sub
143
+ context has tool_is_sensitive && context.tool_is_sensitive == true &&
144
+ (
145
+ !(context has principal && context.principal has act_sub) ||
146
+ (
147
+ context has agent_type && context.agent_type == "autonomous" &&
148
+ !(context has agent_trust_level && context.agent_trust_level == "first_party")
149
+ )
150
+ )
61
151
  };
62
152
 
63
153
  // ---------------------------------------------------------------------------
64
154
  // Section 2: Trust level gates the blast radius
65
155
  // Start in monitor: a service key is unverified until the agent is adopted.
156
+ // Unverified agents are blocked outright, approval or not: registration and
157
+ // adoption come before any per-call authorization.
66
158
  // ---------------------------------------------------------------------------
67
159
 
68
160
  @id("agent-identity.block-unverified-agent-sensitive-tools")
@@ -85,26 +177,29 @@ when {
85
177
  };
86
178
 
87
179
  // ---------------------------------------------------------------------------
88
- // Section 3: Autonomous agents get a higher bar
89
- // Nobody watches an autonomous agent in real time, so a sensitive tool call
90
- // from one requires first-party trust rather than merely "not unverified".
180
+ // Section 3 (folded into Section 1): autonomous agents need first-party trust
181
+ // This template used to forbid sensitive tool calls from autonomous agents
182
+ // that are not first-party. A forbid would deny the very calls Section 1
183
+ // routes to approval, so the condition now lives in Section 1 and those
184
+ // calls require approval instead of being blocked. To hard-block them anyway
185
+ // (overriding approvals and grants), add:
186
+ //
187
+ // @id("agent-identity.restrict-autonomous-agent-sensitive-tools")
188
+ // @name("Block non-first-party autonomous agent tool use")
189
+ // @description("Blocks call_tool on a sensitive tool when agent_type is autonomous unless agent_trust_level is first_party.")
190
+ // @severity("high")
191
+ // @tags("category:agent-identity,threat:escalation,detection:rule,surface:call-tool,scope:per-agent,owasp:asi04")
192
+ // @reject_message("Tool execution blocked: autonomous agents must be first-party to call sensitive tools.")
193
+ // forbid (
194
+ // principal,
195
+ // action == Guardrails::Action::"call_tool",
196
+ // resource
197
+ // )
198
+ // when {
199
+ // context has agent_type && context.agent_type == "autonomous" &&
200
+ // context has tool_is_sensitive && context.tool_is_sensitive == true
201
+ // }
202
+ // unless {
203
+ // context has agent_trust_level && context.agent_trust_level == "first_party"
204
+ // };
91
205
  // ---------------------------------------------------------------------------
92
-
93
- @id("agent-identity.restrict-autonomous-agent-sensitive-tools")
94
- @name("Block non-first-party autonomous agent tool use")
95
- @description("Blocks call_tool on a sensitive tool when agent_type is autonomous unless agent_trust_level is first_party.")
96
- @severity("high")
97
- @tags("category:agent-identity,threat:escalation,detection:rule,surface:call-tool,scope:per-agent,owasp:asi04")
98
- @reject_message("Tool execution blocked: autonomous agents must be first-party to call sensitive tools. No human is in the loop to catch a mistake.")
99
- forbid (
100
- principal,
101
- action == Guardrails::Action::"call_tool",
102
- resource
103
- )
104
- when {
105
- context has agent_type && context.agent_type == "autonomous" &&
106
- context has tool_is_sensitive && context.tool_is_sensitive == true
107
- }
108
- unless {
109
- context has agent_trust_level && context.agent_trust_level == "first_party"
110
- };