@highflame/policy 2.2.41 → 2.2.43

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.
Files changed (32) hide show
  1. package/_schemas/agent_ops/context.json +0 -42
  2. package/_schemas/agent_ops/schema.cedarschema +44 -6
  3. package/_schemas/ai_gateway/context.json +300 -0
  4. package/_schemas/ai_gateway/schema.cedarschema +107 -3
  5. package/_schemas/guardrails/context.json +0 -36
  6. package/_schemas/guardrails/detectors.json +0 -1
  7. package/_schemas/guardrails/schema.cedarschema +43 -5
  8. package/_schemas/guardrails/templates/dual_attribution.cedar +110 -0
  9. package/_schemas/guardrails/templates/multi_turn_trajectory.cedar +116 -0
  10. package/_schemas/guardrails/templates/profiles/output_protection/credentials.cedar +35 -0
  11. package/_schemas/guardrails/templates/profiles/output_protection/data_leakage.cedar +63 -0
  12. package/_schemas/guardrails/templates/profiles/output_protection/harmful_content.cedar +58 -0
  13. package/_schemas/guardrails/templates/profiles/output_protection/injection_carried_back.cedar +39 -0
  14. package/_schemas/guardrails/templates/session_risk_accumulation.cedar +103 -0
  15. package/_schemas/guardrails/templates/templates.json +157 -11
  16. package/_schemas/overwatch/context.json +65 -0
  17. package/_schemas/overwatch/detectors.json +47 -0
  18. package/_schemas/overwatch/schema.cedarschema +90 -0
  19. package/dist/agent_ops-context.gen.d.ts +1 -2
  20. package/dist/agent_ops-context.gen.js +0 -2
  21. package/dist/ai_gateway-entities.gen.js +5 -1
  22. package/dist/guardrails-context.gen.d.ts +1 -2
  23. package/dist/guardrails-context.gen.js +0 -2
  24. package/dist/guardrails-defaults.gen.js +751 -11
  25. package/dist/overwatch-context.gen.d.ts +3 -1
  26. package/dist/overwatch-context.gen.js +4 -0
  27. package/dist/overwatch-detectors.gen.js +16 -0
  28. package/dist/parser.d.ts +2 -1
  29. package/dist/parser.js +124 -37
  30. package/dist/service-schemas.gen.d.ts +4 -4
  31. package/dist/service-schemas.gen.js +354 -32
  32. package/package.json +1 -1
@@ -25,12 +25,6 @@
25
25
  "required": false,
26
26
  "description": "Principal identity class projected from the token: 'human', 'agent', or 'service'. Use to apply identity-class-specific policies"
27
27
  },
28
- {
29
- "key": "principal",
30
- "type": "string",
31
- "required": false,
32
- "description": "Stable principal identifier projected from the token (e.g. a ZeroID / WIMSE URI or user id). Absent when the token carries no principal claim"
33
- },
34
28
  {
35
29
  "key": "request_id",
36
30
  "type": "string",
@@ -696,12 +690,6 @@
696
690
  "required": false,
697
691
  "description": "Principal identity class projected from the token: 'human', 'agent', or 'service'. Use to apply identity-class-specific policies"
698
692
  },
699
- {
700
- "key": "principal",
701
- "type": "string",
702
- "required": false,
703
- "description": "Stable principal identifier projected from the token (e.g. a ZeroID / WIMSE URI or user id). Absent when the token carries no principal claim"
704
- },
705
693
  {
706
694
  "key": "request_id",
707
695
  "type": "string",
@@ -1367,12 +1355,6 @@
1367
1355
  "required": false,
1368
1356
  "description": "Principal identity class projected from the token: 'human', 'agent', or 'service'. Use to apply identity-class-specific policies"
1369
1357
  },
1370
- {
1371
- "key": "principal",
1372
- "type": "string",
1373
- "required": false,
1374
- "description": "Stable principal identifier projected from the token (e.g. a ZeroID / WIMSE URI or user id). Absent when the token carries no principal claim"
1375
- },
1376
1358
  {
1377
1359
  "key": "request_id",
1378
1360
  "type": "string",
@@ -2098,12 +2080,6 @@
2098
2080
  "required": false,
2099
2081
  "description": "Principal identity class projected from the token: 'human', 'agent', or 'service'. Use to apply identity-class-specific policies"
2100
2082
  },
2101
- {
2102
- "key": "principal",
2103
- "type": "string",
2104
- "required": false,
2105
- "description": "Stable principal identifier projected from the token (e.g. a ZeroID / WIMSE URI or user id). Absent when the token carries no principal claim"
2106
- },
2107
2083
  {
2108
2084
  "key": "request_id",
2109
2085
  "type": "string",
@@ -2345,12 +2321,6 @@
2345
2321
  "required": false,
2346
2322
  "description": "Principal identity class projected from the token: 'human', 'agent', or 'service'. Use to apply identity-class-specific policies"
2347
2323
  },
2348
- {
2349
- "key": "principal",
2350
- "type": "string",
2351
- "required": false,
2352
- "description": "Stable principal identifier projected from the token (e.g. a ZeroID / WIMSE URI or user id). Absent when the token carries no principal claim"
2353
- },
2354
2324
  {
2355
2325
  "key": "request_id",
2356
2326
  "type": "string",
@@ -2605,12 +2575,6 @@
2605
2575
  "required": false,
2606
2576
  "description": "Principal identity class projected from the token: 'human', 'agent', or 'service'. Use to apply identity-class-specific policies"
2607
2577
  },
2608
- {
2609
- "key": "principal",
2610
- "type": "string",
2611
- "required": false,
2612
- "description": "Stable principal identifier projected from the token (e.g. a ZeroID / WIMSE URI or user id). Absent when the token carries no principal claim"
2613
- },
2614
2578
  {
2615
2579
  "key": "request_id",
2616
2580
  "type": "string",
@@ -1742,7 +1742,6 @@
1742
1742
  "param_type_violation",
1743
1743
  "param_type_violations",
1744
1744
  "path",
1745
- "principal",
1746
1745
  "privilege_scope",
1747
1746
  "request_id",
1748
1747
  "role",
@@ -105,6 +105,44 @@ namespace Guardrails {
105
105
  // Context Types (Action-Specific)
106
106
  // =========================================================================
107
107
 
108
+ /// The ZeroID NHI principal block (ADR 0009 / CAP-IDN-011).
109
+ ///
110
+ /// A record, not a string: Shield projects the whole identity block under
111
+ /// `context.principal`, so policies read members off it —
112
+ ///
113
+ /// permit ... when { context has principal &&
114
+ /// context.principal.trust_level == "first_party" };
115
+ /// forbid ... unless { context has principal &&
116
+ /// context.principal has act_sub };
117
+ ///
118
+ /// Declared once and shared by every action context so a new member cannot
119
+ /// land on one action and go missing on another.
120
+ ///
121
+ /// `act` (RFC 8693 delegation) is flattened to `act_sub` / `act_iss`
122
+ /// because Cedar records are flat key/value at each level; the full
123
+ /// delegation tree is reconstructable audit-side by joining on `mission_id`.
124
+ ///
125
+ /// Every member is optional except `delegation_depth`: the projector omits
126
+ /// a field whose value is empty, but always emits the depth because zero is
127
+ /// the meaningful base case for a direct (non-delegated) token. The record
128
+ /// itself is optional — it is absent, not empty, when the request carries no
129
+ /// NHI claims (RS256 human tokens, internal-service callers), so
130
+ /// `context has principal` is false rather than true-but-empty.
131
+ type PrincipalContext = {
132
+ "identity_type"?: String, // "human" | "agent" | "service"
133
+ "sub_type"?: String, // Identity subtype (e.g. "coding_agent")
134
+ "trust_level"?: String, // "first_party" | "verified" | "unverified"
135
+ "framework"?: String, // Agent framework (e.g. "langchain")
136
+ "publisher"?: String, // Agent publisher
137
+ "capabilities"?: Set<String>, // Capabilities granted to the identity
138
+ "scopes"?: Set<String>, // OAuth scopes carried by the token
139
+ "delegation_depth": Long, // Delegation hops; 0 for a direct token
140
+ "act_sub"?: String, // RFC 8693 actor — the accountable human
141
+ "act_iss"?: String, // Issuer of the actor claim
142
+ "mission_id"?: String, // Correlates one delegation tree
143
+ "grant_type"?: String, // How the token was obtained (e.g. "api_key")
144
+ };
145
+
108
146
  /// Context for the process_response action.
109
147
  ///
110
148
  /// An alias, not a copy: prompts and responses are inspected by the same
@@ -121,7 +159,7 @@ namespace Guardrails {
121
159
  "role"?: String,
122
160
  "privilege_scope"?: Set<String>,
123
161
  "identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
124
- "principal"?: String, // Stable principal identifier (e.g. ZeroID / WIMSE URI or user id)
162
+ "principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
125
163
  // Core metadata (required)
126
164
  "request_id": String,
127
165
  "timestamp": Long,
@@ -273,7 +311,7 @@ namespace Guardrails {
273
311
  "role"?: String,
274
312
  "privilege_scope"?: Set<String>,
275
313
  "identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
276
- "principal"?: String, // Stable principal identifier (e.g. ZeroID / WIMSE URI or user id)
314
+ "principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
277
315
  // Core metadata (required)
278
316
  "request_id": String,
279
317
  "timestamp": Long,
@@ -469,7 +507,7 @@ namespace Guardrails {
469
507
  "role"?: String,
470
508
  "privilege_scope"?: Set<String>,
471
509
  "identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
472
- "principal"?: String, // Stable principal identifier (e.g. ZeroID / WIMSE URI or user id)
510
+ "principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
473
511
  // Core metadata (required)
474
512
  "request_id": String,
475
513
  "timestamp": Long,
@@ -542,7 +580,7 @@ namespace Guardrails {
542
580
  "role"?: String,
543
581
  "privilege_scope"?: Set<String>,
544
582
  "identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
545
- "principal"?: String, // Stable principal identifier (e.g. ZeroID / WIMSE URI or user id)
583
+ "principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
546
584
  // Core metadata (required)
547
585
  "request_id": String,
548
586
  "timestamp": Long,
@@ -619,7 +657,7 @@ namespace Guardrails {
619
657
  "role"?: String,
620
658
  "privilege_scope"?: Set<String>,
621
659
  "identity_type"?: String, // Principal identity class: "human" | "agent" | "service"
622
- "principal"?: String, // Stable principal identifier (e.g. ZeroID / WIMSE URI or user id)
660
+ "principal"?: PrincipalContext, // ZeroID NHI identity block; absent when the token carries no NHI claims
623
661
  // Core metadata (required)
624
662
  "request_id": String,
625
663
  "timestamp": Long,
@@ -0,0 +1,110 @@
1
+ // =============================================================================
2
+ // Dual Attribution
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.
9
+ //
10
+ // The agent comes from the authenticated identity, never from the request
11
+ // 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.
14
+ //
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
+ //
21
+ // Detection layers:
22
+ // - agent identity (authentication layer, always available)
23
+ // - tool_validator (tool_is_sensitive, tool_category, always available)
24
+ //
25
+ // Context keys consumed:
26
+ // - agent_id, agent_type, agent_trust_level: String
27
+ // - principal: record — act_sub names the accountable human
28
+ // - tool_is_sensitive: Bool
29
+ // - tool_category: String
30
+ //
31
+ // Compliance:
32
+ // - OWASP ASI01, OWASP ASI04
33
+ //
34
+ // Category: agent-identity
35
+ // Namespace: Guardrails
36
+ // =============================================================================
37
+
38
+ // ---------------------------------------------------------------------------
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.
42
+ // ---------------------------------------------------------------------------
43
+
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.")
47
+ @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 (
51
+ principal,
52
+ action == Guardrails::Action::"call_tool",
53
+ resource
54
+ )
55
+ when {
56
+ 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
61
+ };
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Section 2: Trust level gates the blast radius
65
+ // Start in monitor: a service key is unverified until the agent is adopted.
66
+ // ---------------------------------------------------------------------------
67
+
68
+ @id("agent-identity.block-unverified-agent-sensitive-tools")
69
+ @name("Block unverified agents from sensitive tools")
70
+ @description("Blocks call_tool when agent_trust_level is unverified and the tool is sensitive or dangerous.")
71
+ @severity("critical")
72
+ @tags("category:agent-identity,detection:rule,surface:call-tool,scope:per-agent,posture:deny-default,owasp:asi01")
73
+ @reject_message("Tool execution blocked: unverified agents may not call sensitive or dangerous tools. Register and adopt the agent to raise its trust level.")
74
+ forbid (
75
+ principal,
76
+ action == Guardrails::Action::"call_tool",
77
+ resource
78
+ )
79
+ when {
80
+ context has agent_trust_level && context.agent_trust_level == "unverified" &&
81
+ (
82
+ (context has tool_is_sensitive && context.tool_is_sensitive == true) ||
83
+ (context has tool_category && context.tool_category == "dangerous")
84
+ )
85
+ };
86
+
87
+ // ---------------------------------------------------------------------------
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".
91
+ // ---------------------------------------------------------------------------
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
+ };
@@ -0,0 +1,116 @@
1
+ // =============================================================================
2
+ // Multi-Turn Trajectory Escalation
3
+ // =============================================================================
4
+ // Blocks a conversation whose TRAJECTORY is an attack, even when no single
5
+ // message is. A crescendo attack never sends a message alarming enough to
6
+ // block on its own; it walks the model there in small, plausible steps and
7
+ // asks for the payoff in a sentence that would pass any single-message filter.
8
+ //
9
+ // Shield scores every turn twice and exposes both scores separately:
10
+ // - injection_pulse_score / jailbreak_pulse_score — the single-turn
11
+ // classifier. Sees only the current message.
12
+ // - injection_deep_context_score / jailbreak_deep_context_score — the
13
+ // multi-turn model, which carries hidden state across turns keyed on
14
+ // session_id. Sees the whole conversation.
15
+ //
16
+ // Because they are separate keys, a rule can fire on the GAP between them:
17
+ // high trajectory score, low message score. In plain language, "the history
18
+ // is an attack and this message is not" — a condition a filter that only ever
19
+ // has one number cannot express. Section 1 is that rule. Section 2 is a safety
20
+ // net for a trajectory blatant enough to stand on its own.
21
+ //
22
+ // Requires a stable session_id on every request in the conversation. With no
23
+ // session_id there is no threaded state, multi_turn_detection is false, and
24
+ // none of these rules can fire.
25
+ //
26
+ // Detection layers:
27
+ // - injection (single-turn classifier, always available)
28
+ // - deepcontext (multi-turn model, always available)
29
+ //
30
+ // Context keys consumed:
31
+ // - multi_turn_detection: Bool — true only when threaded state was used
32
+ // - injection_deep_context_score, injection_pulse_score: Long (0-100)
33
+ // - jailbreak_deep_context_score, jailbreak_pulse_score: Long (0-100)
34
+ //
35
+ // Compliance:
36
+ // - OWASP LLM01, OWASP LLM02, MITRE ATLAS AML.T0051, AML.T0054
37
+ //
38
+ // Category: security
39
+ // Namespace: Guardrails
40
+ // =============================================================================
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Section 1: Trajectory/turn divergence
44
+ // The conversation scores as an attack; this message does not.
45
+ // ---------------------------------------------------------------------------
46
+
47
+ @id("security.block-trajectory-injection-divergence")
48
+ @name("Block multi-turn injection the current turn hides")
49
+ @description("Blocks process_prompt and process_response when threaded state is in use, injection_deep_context_score >= 60, and injection_pulse_score <= 39.")
50
+ @severity("critical")
51
+ @tags("category:security,threat:injection,detection:ml,surface:process-prompt,surface:process-response,scope:multi-turn,owasp:llm01,mitre:atlas-t0051")
52
+ @reject_message("Prompt blocked: this conversation is a progressive prompt-injection attempt. No single message crossed the line; the sequence did.")
53
+ forbid (
54
+ principal,
55
+ action in [Guardrails::Action::"process_prompt", Guardrails::Action::"process_response"],
56
+ resource
57
+ )
58
+ when {
59
+ context has multi_turn_detection && context.multi_turn_detection == true &&
60
+ context has injection_deep_context_score && context.injection_deep_context_score >= 60 &&
61
+ context has injection_pulse_score && context.injection_pulse_score <= 39
62
+ };
63
+
64
+ @id("security.block-trajectory-jailbreak-divergence")
65
+ @name("Block multi-turn jailbreak the current turn hides")
66
+ @description("Blocks process_prompt and process_response when threaded state is in use, jailbreak_deep_context_score >= 60, and jailbreak_pulse_score <= 39.")
67
+ @severity("critical")
68
+ @tags("category:security,threat:jailbreak,detection:ml,surface:process-prompt,surface:process-response,scope:multi-turn,owasp:llm02,mitre:atlas-t0054")
69
+ @reject_message("Prompt blocked: this conversation is a progressive jailbreak. No single message crossed the line; the sequence did.")
70
+ forbid (
71
+ principal,
72
+ action in [Guardrails::Action::"process_prompt", Guardrails::Action::"process_response"],
73
+ resource
74
+ )
75
+ when {
76
+ context has multi_turn_detection && context.multi_turn_detection == true &&
77
+ context has jailbreak_deep_context_score && context.jailbreak_deep_context_score >= 60 &&
78
+ context has jailbreak_pulse_score && context.jailbreak_pulse_score <= 39
79
+ };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Section 2: Blatant trajectory, regardless of the message score
83
+ // Covers the band between Section 1 and the default single-turn thresholds.
84
+ // ---------------------------------------------------------------------------
85
+
86
+ @id("security.block-trajectory-injection-high")
87
+ @name("Block high-confidence multi-turn injection")
88
+ @description("Blocks process_prompt and process_response when threaded state is in use and injection_deep_context_score >= 75.")
89
+ @severity("high")
90
+ @tags("category:security,threat:injection,detection:ml,surface:process-prompt,surface:process-response,scope:multi-turn,owasp:llm01")
91
+ @reject_message("Prompt blocked: the multi-turn model scored this conversation as a prompt-injection attempt with high confidence.")
92
+ forbid (
93
+ principal,
94
+ action in [Guardrails::Action::"process_prompt", Guardrails::Action::"process_response"],
95
+ resource
96
+ )
97
+ when {
98
+ context has multi_turn_detection && context.multi_turn_detection == true &&
99
+ context has injection_deep_context_score && context.injection_deep_context_score >= 75
100
+ };
101
+
102
+ @id("security.block-trajectory-jailbreak-high")
103
+ @name("Block high-confidence multi-turn jailbreak")
104
+ @description("Blocks process_prompt and process_response when threaded state is in use and jailbreak_deep_context_score >= 75.")
105
+ @severity("high")
106
+ @tags("category:security,threat:jailbreak,detection:ml,surface:process-prompt,surface:process-response,scope:multi-turn,owasp:llm02")
107
+ @reject_message("Prompt blocked: the multi-turn model scored this conversation as a jailbreak attempt with high confidence.")
108
+ forbid (
109
+ principal,
110
+ action in [Guardrails::Action::"process_prompt", Guardrails::Action::"process_response"],
111
+ resource
112
+ )
113
+ when {
114
+ context has multi_turn_detection && context.multi_turn_detection == true &&
115
+ context has jailbreak_deep_context_score && context.jailbreak_deep_context_score >= 75
116
+ };
@@ -0,0 +1,35 @@
1
+ // =============================================================================
2
+ // Output Protection — Credentials
3
+ // =============================================================================
4
+ // Split from data_leakage.cedar: secrets are category `data-protection`, PII is
5
+ // `privacy`, and a template wrapper may only carry rules of its own category
6
+ // (HFP-LINT-TMPL-006). Relabelling one to fit the other would put a wrong
7
+ // category on the wire, where it drives signals[] and the severity rollup.
8
+ //
9
+ // Same scoping rule as the rest of this profile — process_response ALONE, so a
10
+ // tenant can police what the model says without touching what users send.
11
+ //
12
+ // Context keys consumed:
13
+ // - secrets_detected: Bool
14
+ //
15
+ // Compliance:
16
+ // - OWASP LLM02 (Sensitive Information Disclosure)
17
+ //
18
+ // Category: data-protection
19
+ // Namespace: Guardrails
20
+ // =============================================================================
21
+
22
+ @id("data-protection.output-block-secrets")
23
+ @name("Block secrets in model responses")
24
+ @description("Blocks process_response when the model's own output contains credentials, API keys or tokens — the canonical way a leaked secret reaches a caller.")
25
+ @severity("critical")
26
+ @tags("category:data-protection,threat:secrets,surface:process-response,detection:rule,owasp:llm02")
27
+ @reject_message("Response blocked: the model's output contained credentials.")
28
+ forbid (
29
+ principal,
30
+ action == Guardrails::Action::"process_response",
31
+ resource
32
+ )
33
+ when {
34
+ context has secrets_detected && context.secrets_detected == true
35
+ };
@@ -0,0 +1,63 @@
1
+ // =============================================================================
2
+ // Output Protection — Data Leakage
3
+ // =============================================================================
4
+ // Guards what the MODEL SAYS BACK, not what the user sends in.
5
+ //
6
+ // Every other guardrails template binds process_response alongside
7
+ // process_prompt and the tool/file actions, which is right for a threat that
8
+ // is the same in both directions. Data leakage is not that: the risk is
9
+ // specific to egress, and a tenant who wants to police only their outputs had
10
+ // no template to enable. That gap is why one deployed tenant hand-authored a
11
+ // rule named "Block PII in outputs" and bound it to process_prompt — the only
12
+ // action that covered responses before the direction split (ADR 0031).
13
+ //
14
+ // These rules bind process_response ALONE. That is the point: ADR 0031 created
15
+ // the distinct trigger precisely so a policy could be scoped to one direction
16
+ // from its head, and this profile is what that is for. Enabling it cannot
17
+ // change how prompts are treated.
18
+ //
19
+ // Context keys consumed:
20
+ // - pii_detected: Bool
21
+ // - pii_count: Long
22
+ //
23
+ // Compliance:
24
+ // - OWASP LLM02 (Sensitive Information Disclosure), OWASP LLM06
25
+ //
26
+ // Category: privacy
27
+ // Namespace: Guardrails
28
+ // =============================================================================
29
+
30
+ @id("privacy.output-block-pii")
31
+ @name("Block PII in model responses")
32
+ @description("Blocks process_response when the model's own output contains PII. Scoped to the response direction only — prompts are unaffected.")
33
+ @severity("high")
34
+ @tags("category:privacy,threat:data-leak,surface:process-response,detection:rule,owasp:llm02")
35
+ @reject_message("Response blocked: the model's output contained personal data.")
36
+ forbid (
37
+ principal,
38
+ action == Guardrails::Action::"process_response",
39
+ resource
40
+ )
41
+ when {
42
+ context has pii_detected && context.pii_detected == true
43
+ };
44
+
45
+ // Bulk disclosure is a separate finding from a single incidental match: a
46
+ // response carrying many distinct PII items is an exfiltration shape rather
47
+ // than a mention, and is worth its own severity and message even though the
48
+ // rule above already blocks it. Kept as a distinct rule so a tenant running
49
+ // the first in monitor mode can still enforce on bulk.
50
+ @id("privacy.output-block-bulk-pii")
51
+ @name("Block bulk PII disclosure in model responses")
52
+ @description("Blocks process_response when the output carries five or more PII items — a disclosure shape rather than an incidental mention.")
53
+ @severity("critical")
54
+ @tags("category:privacy,threat:data-leak,surface:process-response,detection:rule,owasp:llm06")
55
+ @reject_message("Response blocked: the model's output contained bulk personal data.")
56
+ forbid (
57
+ principal,
58
+ action == Guardrails::Action::"process_response",
59
+ resource
60
+ )
61
+ when {
62
+ context has pii_count && context.pii_count >= 5
63
+ };
@@ -0,0 +1,58 @@
1
+ // =============================================================================
2
+ // Output Protection — Harmful Content
3
+ // =============================================================================
4
+ // The companion to data_leakage.cedar: what the model says back, judged for
5
+ // safety rather than for disclosure.
6
+ //
7
+ // Same scoping rule and same reason — process_response ALONE (ADR 0031). A
8
+ // tenant enabling this is saying "hold my model's answers to a standard",
9
+ // which is a different decision from "police what users send me", and the two
10
+ // should be separately enableable.
11
+ //
12
+ // Thresholds match the org-wide toxicity defaults rather than tightening them.
13
+ // An output profile that silently enforced stricter limits than the tenant's
14
+ // existing prompt rules would surprise anyone comparing the two.
15
+ //
16
+ // Context keys consumed:
17
+ // - violence_score: Long (0-100)
18
+ // - hate_speech_score: Long (0-100)
19
+ // - sexual_score: Long (0-100)
20
+ // - injection_score: Long (0-100)
21
+ //
22
+ // Compliance:
23
+ // - OWASP LLM05 (Improper Output Handling), OWASP LLM01
24
+ //
25
+ // Category: trust-safety
26
+ // Namespace: Guardrails
27
+ // =============================================================================
28
+
29
+ @id("trust-safety.output-block-toxic")
30
+ @name("Block toxic model responses")
31
+ @description("Blocks process_response when the model's own output scores high for violence or hate speech.")
32
+ @severity("high")
33
+ @tags("category:trust-safety,threat:harmful,surface:process-response,detection:ml,owasp:llm05")
34
+ @reject_message("Response blocked: the model's output was flagged as harmful.")
35
+ forbid (
36
+ principal,
37
+ action == Guardrails::Action::"process_response",
38
+ resource
39
+ )
40
+ when {
41
+ context has violence_score && context.violence_score >= 70 ||
42
+ context has hate_speech_score && context.hate_speech_score >= 70
43
+ };
44
+
45
+ @id("trust-safety.output-block-sexual")
46
+ @name("Block sexual content in model responses")
47
+ @description("Blocks process_response when the model's own output scores high for sexual content.")
48
+ @severity("critical")
49
+ @tags("category:trust-safety,threat:sexual,surface:process-response,detection:ml,owasp:llm05")
50
+ @reject_message("Response blocked: the model's output was flagged as explicit.")
51
+ forbid (
52
+ principal,
53
+ action == Guardrails::Action::"process_response",
54
+ resource
55
+ )
56
+ when {
57
+ context has sexual_score && context.sexual_score >= 91
58
+ };
@@ -0,0 +1,39 @@
1
+ // =============================================================================
2
+ // Output Protection — Injection Carried Back
3
+ // =============================================================================
4
+ // Split from harmful_content.cedar because its category is `security`, not
5
+ // `trust-safety`, and a template wrapper may only carry rules of its own
6
+ // category (HFP-LINT-TMPL-006). The split is the honest outcome: this is a
7
+ // different threat with a different owner, not a toxicity variant.
8
+ //
9
+ // Same scoping rule as the rest of this profile — process_response ALONE.
10
+ //
11
+ // Context keys consumed:
12
+ // - injection_score: Long (0-100)
13
+ //
14
+ // Compliance:
15
+ // - OWASP LLM05 (Improper Output Handling)
16
+ //
17
+ // Category: security
18
+ // Namespace: Guardrails
19
+ // =============================================================================
20
+
21
+ // Injection scored on an OUTPUT is a different finding from injection scored on
22
+ // a prompt. On the way in it is a user attacking the model; on the way out it
23
+ // is content the model is handing to the caller — a downstream agent, a
24
+ // renderer, or another tool — which is OWASP LLM05's improper output handling.
25
+ // Worth its own rule so the two can be tuned and reasoned about separately.
26
+ @id("security.output-block-injection-carried-back")
27
+ @name("Block injection payloads carried back in model responses")
28
+ @description("Blocks process_response when the output itself scores as an injection payload — content the caller may execute or forward.")
29
+ @severity("high")
30
+ @tags("category:security,threat:injection,surface:process-response,detection:ml,owasp:llm05")
31
+ @reject_message("Response blocked: the model's output contained an injection payload.")
32
+ forbid (
33
+ principal,
34
+ action == Guardrails::Action::"process_response",
35
+ resource
36
+ )
37
+ when {
38
+ context has injection_score && context.injection_score >= 80
39
+ };