@highflame/policy 2.2.40 → 2.2.42

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 (33) hide show
  1. package/_schemas/agent_ops/templates/ported/organization/organization_deny_baseline.cedar +2 -2
  2. package/_schemas/agent_ops/templates/ported/tool-permissioning/tools_mcp_server_allowlist.cedar +24 -4
  3. package/_schemas/ai_gateway/context.json +300 -0
  4. package/_schemas/ai_gateway/detectors.json +928 -0
  5. package/_schemas/ai_gateway/schema.cedarschema +107 -3
  6. package/_schemas/ai_gateway/templates/mcp_server_allowlist.cedar +24 -4
  7. package/_schemas/guardrails/detectors.json +2193 -0
  8. package/_schemas/guardrails/templates/mcp_server_allowlist.cedar +24 -4
  9. package/_schemas/guardrails/templates/profiles/output_protection/credentials.cedar +35 -0
  10. package/_schemas/guardrails/templates/profiles/output_protection/data_leakage.cedar +63 -0
  11. package/_schemas/guardrails/templates/profiles/output_protection/harmful_content.cedar +58 -0
  12. package/_schemas/guardrails/templates/profiles/output_protection/injection_carried_back.cedar +39 -0
  13. package/_schemas/guardrails/templates/templates.json +107 -11
  14. package/_schemas/overwatch/context.json +65 -0
  15. package/_schemas/overwatch/detectors.json +1129 -0
  16. package/_schemas/overwatch/schema.cedarschema +90 -0
  17. package/_schemas/sentry/detectors.json +857 -0
  18. package/dist/agent_ops-defaults.gen.js +26 -6
  19. package/dist/ai_gateway-defaults.gen.js +24 -4
  20. package/dist/ai_gateway-detectors.gen.d.ts +1 -1
  21. package/dist/ai_gateway-detectors.gen.js +22 -8
  22. package/dist/ai_gateway-entities.gen.js +5 -1
  23. package/dist/guardrails-defaults.gen.js +366 -15
  24. package/dist/guardrails-detectors.gen.d.ts +1 -1
  25. package/dist/guardrails-detectors.gen.js +22 -8
  26. package/dist/overwatch-context.gen.d.ts +3 -1
  27. package/dist/overwatch-context.gen.js +4 -0
  28. package/dist/overwatch-defaults.gen.js +25 -5
  29. package/dist/overwatch-detectors.gen.d.ts +1 -1
  30. package/dist/overwatch-detectors.gen.js +38 -8
  31. package/dist/service-schemas.gen.d.ts +2 -2
  32. package/dist/service-schemas.gen.js +267 -8
  33. package/package.json +1 -1
@@ -2,8 +2,23 @@
2
2
  // MCP Server Allowlist
3
3
  // =============================================================================
4
4
  // Restricts MCP server connections to a pre-approved list. Customize the
5
- // `context.mcp_server` values in the permit rule to match the allowed
6
- // servers for your environment.
5
+ // `context.mcp_server` values to match the allowed servers for your
6
+ // environment the SAME list appears in both rules and both must be edited.
7
+ //
8
+ // Both rules are conditional on the allowlist, and both are needed:
9
+ //
10
+ // permit ... when { allowlisted } grants access; Cedar is default-deny,
11
+ // so without this nothing allows the
12
+ // allowlisted servers either.
13
+ // forbid ... unless { allowlisted } claws back everything else, including
14
+ // when a broad Baseline Permit is loaded.
15
+ //
16
+ // The forbid MUST carry the negation. `forbid` always beats `permit` in Cedar,
17
+ // so an UNCONDITIONAL forbid denies the allowlisted servers too — which is
18
+ // what this template used to do (highflame-policy#186).
19
+ //
20
+ // The `context has mcp_server` guard stays inside both conditions: a request
21
+ // naming no server cannot be on the allowlist, so it is denied.
7
22
  //
8
23
  // Context keys consumed:
9
24
  // - mcp_server: String
@@ -30,7 +45,7 @@ when {
30
45
 
31
46
  @id("tools.deny-non-allowlisted-mcp")
32
47
  @name("Block non-allowlisted MCP servers")
33
- @description("Blocks connect_server unconditionally so only the allowlist permit applies.")
48
+ @description("Blocks connect_server unless mcp_server is in the allowlist.")
34
49
  @severity("medium")
35
50
  @tags("category:tools,surface:connect-server,scope:org-wide,posture:deny-default")
36
51
  @reject_message("MCP server connection blocked: server is not on the allowlist.")
@@ -38,4 +53,9 @@ forbid (
38
53
  principal,
39
54
  action == Guardrails::Action::"connect_server",
40
55
  resource
41
- );
56
+ )
57
+ unless {
58
+ context has mcp_server &&
59
+ (context.mcp_server == "filesystem" ||
60
+ context.mcp_server == "playwright")
61
+ };
@@ -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
+ };
@@ -60,7 +60,10 @@
60
60
  "category": "organization",
61
61
  "file": "defaults/baseline.cedar",
62
62
  "severity": "low",
63
- "tags": ["category:organization", "posture:permit-default"],
63
+ "tags": [
64
+ "category:organization",
65
+ "posture:permit-default"
66
+ ],
64
67
  "is_active": true
65
68
  }
66
69
  ],
@@ -72,7 +75,10 @@
72
75
  "category": "organization",
73
76
  "file": "defaults/baseline.cedar",
74
77
  "severity": "low",
75
- "tags": ["category:organization", "posture:permit-default"],
78
+ "tags": [
79
+ "category:organization",
80
+ "posture:permit-default"
81
+ ],
76
82
  "auto_deploy": true
77
83
  },
78
84
  {
@@ -82,7 +88,11 @@
82
88
  "category": "data-protection",
83
89
  "file": "defaults/secrets.cedar",
84
90
  "severity": "critical",
85
- "tags": ["category:data-protection", "threat:secrets", "owasp:llm06"]
91
+ "tags": [
92
+ "category:data-protection",
93
+ "threat:secrets",
94
+ "owasp:llm06"
95
+ ]
86
96
  },
87
97
  {
88
98
  "id": "security.injection",
@@ -226,7 +236,11 @@
226
236
  "category": "agent-identity",
227
237
  "file": "defaults/agent_identity.cedar",
228
238
  "severity": "critical",
229
- "tags": ["category:agent-identity", "scope:per-agent", "owasp:llm01"]
239
+ "tags": [
240
+ "category:agent-identity",
241
+ "scope:per-agent",
242
+ "owasp:llm01"
243
+ ]
230
244
  },
231
245
  {
232
246
  "id": "tools.mcp-tool-permissions",
@@ -235,7 +249,11 @@
235
249
  "category": "tools",
236
250
  "file": "mcp_tool_permissions.cedar",
237
251
  "severity": "critical",
238
- "tags": ["category:tools", "threat:supply-chain", "posture:deny-default"]
252
+ "tags": [
253
+ "category:tools",
254
+ "threat:supply-chain",
255
+ "posture:deny-default"
256
+ ]
239
257
  },
240
258
  {
241
259
  "id": "tools.mcp-server-allowlist",
@@ -285,7 +303,10 @@
285
303
  "category": "data-protection",
286
304
  "file": "profiles/code_agent/security.cedar",
287
305
  "severity": "critical",
288
- "tags": ["category:data-protection", "threat:secrets"]
306
+ "tags": [
307
+ "category:data-protection",
308
+ "threat:secrets"
309
+ ]
289
310
  },
290
311
  {
291
312
  "id": "security.code-agent-encoding",
@@ -307,7 +328,11 @@
307
328
  "category": "security",
308
329
  "file": "profiles/code_agent/path_security.cedar",
309
330
  "severity": "critical",
310
- "tags": ["category:security", "threat:secrets", "threat:path-traversal"]
331
+ "tags": [
332
+ "category:security",
333
+ "threat:secrets",
334
+ "threat:path-traversal"
335
+ ]
311
336
  },
312
337
  {
313
338
  "id": "agent-security.code-agent",
@@ -346,7 +371,11 @@
346
371
  "category": "data-protection",
347
372
  "file": "profiles/data_pipeline/data_protection.cedar",
348
373
  "severity": "critical",
349
- "tags": ["category:data-protection", "threat:secrets", "owasp:llm06"]
374
+ "tags": [
375
+ "category:data-protection",
376
+ "threat:secrets",
377
+ "owasp:llm06"
378
+ ]
350
379
  },
351
380
  {
352
381
  "id": "security.data-pipeline-block-injection",
@@ -355,7 +384,11 @@
355
384
  "category": "security",
356
385
  "file": "profiles/data_pipeline/security.cedar",
357
386
  "severity": "high",
358
- "tags": ["category:security", "threat:injection", "owasp:llm01"]
387
+ "tags": [
388
+ "category:security",
389
+ "threat:injection",
390
+ "owasp:llm01"
391
+ ]
359
392
  },
360
393
  {
361
394
  "id": "agent-security.data-pipeline",
@@ -364,7 +397,10 @@
364
397
  "category": "agent-security",
365
398
  "file": "profiles/data_pipeline/agentic_security.cedar",
366
399
  "severity": "critical",
367
- "tags": ["category:agent-security", "threat:exfiltration"]
400
+ "tags": [
401
+ "category:agent-security",
402
+ "threat:exfiltration"
403
+ ]
368
404
  },
369
405
  {
370
406
  "id": "agent-identity.multi-agent-trust",
@@ -473,7 +509,11 @@
473
509
  "category": "data-protection",
474
510
  "file": "profiles/advanced_detection/secrets.cedar",
475
511
  "severity": "critical",
476
- "tags": ["category:data-protection", "threat:secrets", "owasp:llm06"]
512
+ "tags": [
513
+ "category:data-protection",
514
+ "threat:secrets",
515
+ "owasp:llm06"
516
+ ]
477
517
  },
478
518
  {
479
519
  "id": "privacy.advanced-pii",
@@ -503,6 +543,62 @@
503
543
  "aarm:r3",
504
544
  "posture:deny-default"
505
545
  ]
546
+ },
547
+ {
548
+ "id": "privacy.output-protection",
549
+ "name": "Output Protection — Data Leakage",
550
+ "description": "Block PII, secrets and bulk disclosure in the model's own responses. Scoped to the response direction only, so prompts are unaffected.",
551
+ "category": "privacy",
552
+ "file": "profiles/output_protection/data_leakage.cedar",
553
+ "severity": "critical",
554
+ "tags": [
555
+ "category:privacy",
556
+ "threat:data-leak",
557
+ "surface:process-response",
558
+ "detection:rule"
559
+ ]
560
+ },
561
+ {
562
+ "id": "trust-safety.output-protection",
563
+ "name": "Output Protection — Harmful Content",
564
+ "description": "Hold the model's own responses to a safety standard: toxicity, explicit content, and injection payloads carried back to the caller. Response direction only.",
565
+ "category": "trust-safety",
566
+ "file": "profiles/output_protection/harmful_content.cedar",
567
+ "severity": "critical",
568
+ "tags": [
569
+ "category:trust-safety",
570
+ "threat:harmful",
571
+ "surface:process-response",
572
+ "detection:ml"
573
+ ]
574
+ },
575
+ {
576
+ "id": "security.output-block-injection-carried-back",
577
+ "name": "Output Protection — Injection Carried Back",
578
+ "description": "Block responses that themselves score as an injection payload — content a caller, downstream agent or renderer may execute. Response direction only.",
579
+ "category": "security",
580
+ "file": "profiles/output_protection/injection_carried_back.cedar",
581
+ "severity": "high",
582
+ "tags": [
583
+ "category:security",
584
+ "threat:injection",
585
+ "surface:process-response",
586
+ "detection:ml"
587
+ ]
588
+ },
589
+ {
590
+ "id": "data-protection.output-block-secrets",
591
+ "name": "Output Protection — Credentials",
592
+ "description": "Block responses whose own content contains credentials, API keys or tokens — the canonical way a leaked secret reaches a caller. Response direction only.",
593
+ "category": "data-protection",
594
+ "file": "profiles/output_protection/credentials.cedar",
595
+ "severity": "critical",
596
+ "tags": [
597
+ "category:data-protection",
598
+ "threat:secrets",
599
+ "surface:process-response",
600
+ "detection:rule"
601
+ ]
506
602
  }
507
603
  ]
508
604
  }
@@ -333,6 +333,19 @@
333
333
  "type": "number",
334
334
  "required": false,
335
335
  "description": "Sum of per-turn risk scores across the session. Catches death-by-a-thousand-cuts where no single turn is high but cumulative risk is significant"
336
+ },
337
+ {
338
+ "key": "budget_remaining_pct",
339
+ "type": "number",
340
+ "required": false,
341
+ "description": "Remaining session token budget as a percentage (0-100). Optional, so a policy must guard it: `context has budget_remaining_pct && context.budget_remaining_pct < 10` — a bare comparison fails Cedar validation. Runtime default-fills it to 100 whenever the detector runs, so a high value is not evidence that a budget rule exists.",
342
+ "range": "0-100"
343
+ },
344
+ {
345
+ "key": "budget_exceeded",
346
+ "type": "boolean",
347
+ "required": false,
348
+ "description": "Whether the session token budget has been exceeded. Optional, so a policy must guard it with `context has budget_exceeded`. The guard is a validation requirement, not a fail-closed property: the detector emits false whenever a session loads, including when no budget rule is configured, so a grant conditioned on `!context.budget_exceeded` will fire on unbudgeted sessions."
336
349
  }
337
350
  ]
338
351
  },
@@ -854,6 +867,19 @@
854
867
  "type": "number",
855
868
  "required": false,
856
869
  "description": "Sum of per-turn risk scores across the session. Catches death-by-a-thousand-cuts where no single turn is high but cumulative risk is significant"
870
+ },
871
+ {
872
+ "key": "budget_remaining_pct",
873
+ "type": "number",
874
+ "required": false,
875
+ "description": "Remaining session token budget as a percentage (0-100). Optional, so a policy must guard it: `context has budget_remaining_pct && context.budget_remaining_pct < 10` — a bare comparison fails Cedar validation. Runtime default-fills it to 100 whenever the detector runs, so a high value is not evidence that a budget rule exists.",
876
+ "range": "0-100"
877
+ },
878
+ {
879
+ "key": "budget_exceeded",
880
+ "type": "boolean",
881
+ "required": false,
882
+ "description": "Whether the session token budget has been exceeded. Optional, so a policy must guard it with `context has budget_exceeded`. The guard is a validation requirement, not a fail-closed property: the detector emits false whenever a session loads, including when no budget rule is configured, so a grant conditioned on `!context.budget_exceeded` will fire on unbudgeted sessions."
857
883
  }
858
884
  ]
859
885
  },
@@ -1285,6 +1311,19 @@
1285
1311
  "type": "number",
1286
1312
  "required": false,
1287
1313
  "description": "Sum of per-turn risk scores across the session. Catches death-by-a-thousand-cuts where no single turn is high but cumulative risk is significant"
1314
+ },
1315
+ {
1316
+ "key": "budget_remaining_pct",
1317
+ "type": "number",
1318
+ "required": false,
1319
+ "description": "Remaining session token budget as a percentage (0-100). Optional, so a policy must guard it: `context has budget_remaining_pct && context.budget_remaining_pct < 10` — a bare comparison fails Cedar validation. Runtime default-fills it to 100 whenever the detector runs, so a high value is not evidence that a budget rule exists.",
1320
+ "range": "0-100"
1321
+ },
1322
+ {
1323
+ "key": "budget_exceeded",
1324
+ "type": "boolean",
1325
+ "required": false,
1326
+ "description": "Whether the session token budget has been exceeded. Optional, so a policy must guard it with `context has budget_exceeded`. The guard is a validation requirement, not a fail-closed property: the detector emits false whenever a session loads, including when no budget rule is configured, so a grant conditioned on `!context.budget_exceeded` will fire on unbudgeted sessions."
1288
1327
  }
1289
1328
  ]
1290
1329
  },
@@ -1494,6 +1533,19 @@
1494
1533
  "type": "number",
1495
1534
  "required": false,
1496
1535
  "description": "Sum of per-turn risk scores across the session. Catches death-by-a-thousand-cuts where no single turn is high but cumulative risk is significant"
1536
+ },
1537
+ {
1538
+ "key": "budget_remaining_pct",
1539
+ "type": "number",
1540
+ "required": false,
1541
+ "description": "Remaining session token budget as a percentage (0-100). Optional, so a policy must guard it: `context has budget_remaining_pct && context.budget_remaining_pct < 10` — a bare comparison fails Cedar validation. Runtime default-fills it to 100 whenever the detector runs, so a high value is not evidence that a budget rule exists.",
1542
+ "range": "0-100"
1543
+ },
1544
+ {
1545
+ "key": "budget_exceeded",
1546
+ "type": "boolean",
1547
+ "required": false,
1548
+ "description": "Whether the session token budget has been exceeded. Optional, so a policy must guard it with `context has budget_exceeded`. The guard is a validation requirement, not a fail-closed property: the detector emits false whenever a session loads, including when no budget rule is configured, so a grant conditioned on `!context.budget_exceeded` will fire on unbudgeted sessions."
1497
1549
  }
1498
1550
  ]
1499
1551
  },
@@ -1709,6 +1761,19 @@
1709
1761
  "type": "number",
1710
1762
  "required": false,
1711
1763
  "description": "Sum of per-turn risk scores across the session. Catches death-by-a-thousand-cuts where no single turn is high but cumulative risk is significant"
1764
+ },
1765
+ {
1766
+ "key": "budget_remaining_pct",
1767
+ "type": "number",
1768
+ "required": false,
1769
+ "description": "Remaining session token budget as a percentage (0-100). Optional, so a policy must guard it: `context has budget_remaining_pct && context.budget_remaining_pct < 10` — a bare comparison fails Cedar validation. Runtime default-fills it to 100 whenever the detector runs, so a high value is not evidence that a budget rule exists.",
1770
+ "range": "0-100"
1771
+ },
1772
+ {
1773
+ "key": "budget_exceeded",
1774
+ "type": "boolean",
1775
+ "required": false,
1776
+ "description": "Whether the session token budget has been exceeded. Optional, so a policy must guard it with `context has budget_exceeded`. The guard is a validation requirement, not a fail-closed property: the detector emits false whenever a session loads, including when no budget rule is configured, so a grant conditioned on `!context.budget_exceeded` will fire on unbudgeted sessions."
1712
1777
  }
1713
1778
  ]
1714
1779
  }