@highflame/policy 2.2.45 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_schemas/agent_ops/schema.cedarschema +22 -0
- package/_schemas/ai_gateway/schema.cedarschema +49 -0
- package/_schemas/ai_gateway/templates/single_use_sensitive_tools.cedar +54 -0
- package/_schemas/ai_gateway/templates/templates.json +37 -0
- package/_schemas/ai_gateway/templates/unadopted_agents.cedar +64 -0
- package/_schemas/guardrails/schema.cedarschema +21 -0
- package/_schemas/guardrails/templates/dual_attribution.cedar +143 -48
- package/_schemas/guardrails/templates/single_use_sensitive_tools.cedar +54 -0
- package/_schemas/guardrails/templates/templates.json +34 -2
- package/_schemas/guardrails/templates/unadopted_agents.cedar +64 -0
- package/dist/aarm-annotation.d.ts +58 -4
- package/dist/aarm-annotation.js +177 -4
- package/dist/aarm-annotations.gen.js +85 -1
- package/dist/ai_gateway-defaults.gen.d.ts +1 -1
- package/dist/ai_gateway-defaults.gen.js +176 -0
- package/dist/annotations.d.ts +3 -2
- package/dist/annotations.js +6 -2
- package/dist/guardrails-defaults.gen.js +309 -44
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/service-schemas.gen.d.ts +3 -3
- package/dist/service-schemas.gen.js +92 -0
- package/dist/step-up-role-conflict.d.ts +49 -0
- package/dist/step-up-role-conflict.js +117 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +3 -0
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
5
|
-
// agent did this?" is half an answer; the other half
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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`
|
|
13
|
-
//
|
|
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
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
|
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
|
|
40
|
-
//
|
|
41
|
-
//
|
|
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.
|
|
45
|
-
@name("
|
|
46
|
-
@description("
|
|
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,
|
|
49
|
-
@
|
|
50
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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:
|
|
89
|
-
//
|
|
90
|
-
//
|
|
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
|
-
};
|