@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
@@ -0,0 +1,103 @@
1
+ // =============================================================================
2
+ // Session Risk Accumulation
3
+ // =============================================================================
4
+ // Blocks the privileged action a probing conversation is working toward. A
5
+ // patient attacker expects some turns to be refused; what they want is one
6
+ // tool call at the end — send the email, move the money, read the file. So
7
+ // the useful question at a tool call is not "is this call suspicious?" but
8
+ // "what has this conversation been doing up to now?"
9
+ //
10
+ // Shield accumulates that history on the session and projects it three ways,
11
+ // because attackers come in three shapes:
12
+ // - session_max_* — a high-water mark that never decays. "Did this session
13
+ // EVER cross a line?" Catches the attacker who probes hard, is refused,
14
+ // goes quiet, then calmly asks for the tool.
15
+ // - session_cumulative_risk_score — an uncapped running sum. "How much
16
+ // total pressure has this session applied?" Catches death by a thousand
17
+ // cuts, where no single turn is alarming.
18
+ // - session_threat_turns — a count of turns that tripped a detector. "Is
19
+ // this sustained, or a one-off?" Separates probing from a false positive.
20
+ //
21
+ // Requires a stable session_id on every request, prompts AND tool calls. The
22
+ // tool call must ride the same session as the conversation, or it reads 0.
23
+ //
24
+ // Detection layers:
25
+ // - session (aggregate over detection history, always available)
26
+ // - tool_validator (tool_is_sensitive, always available)
27
+ //
28
+ // Context keys consumed:
29
+ // - session_max_injection_score, session_max_jailbreak_score: Long (0-100)
30
+ // - session_cumulative_risk_score: Long — uncapped sum
31
+ // - session_threat_turns: Long — count
32
+ // - tool_is_sensitive: Bool
33
+ //
34
+ // Compliance:
35
+ // - OWASP LLM01, OWASP LLM06, OWASP ASI01, OWASP ASI04
36
+ //
37
+ // Category: agent-security
38
+ // Namespace: Guardrails
39
+ // =============================================================================
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Section 1: A session that ever crossed the line does not get to act
43
+ // The conversation is what scored high; the tool call is what gets stopped.
44
+ // ---------------------------------------------------------------------------
45
+
46
+ @id("agent-security.block-tool-after-injection-in-session")
47
+ @name("Block tools after an injection or jailbreak in the session")
48
+ @description("Blocks call_tool when session_max_injection_score >= 60 or session_max_jailbreak_score >= 60, because an earlier turn attempted injection or jailbreak.")
49
+ @severity("critical")
50
+ @tags("category:agent-security,threat:escalation,detection:aggregate,surface:call-tool,scope:multi-turn,owasp:llm01")
51
+ @reject_message("Tool execution blocked: an earlier turn in this session attempted prompt injection or jailbreak. Start a new session to use tools.")
52
+ forbid (
53
+ principal,
54
+ action == Guardrails::Action::"call_tool",
55
+ resource
56
+ )
57
+ when {
58
+ (context has session_max_injection_score && context.session_max_injection_score >= 60) ||
59
+ (context has session_max_jailbreak_score && context.session_max_jailbreak_score >= 60)
60
+ };
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Section 2: Accumulated pressure gates sensitive tools
64
+ // No single turn set a max, but the session as a whole kept pushing.
65
+ // ---------------------------------------------------------------------------
66
+
67
+ @id("agent-security.block-sensitive-tool-on-session-risk")
68
+ @name("Block sensitive tools once the session has accumulated risk")
69
+ @description("Blocks call_tool when session_cumulative_risk_score >= 151 and tool_is_sensitive is true.")
70
+ @severity("high")
71
+ @tags("category:agent-security,threat:escalation,detection:aggregate,surface:call-tool,scope:multi-turn,owasp:asi01")
72
+ @reject_message("Tool execution blocked: this session has accumulated significant risk across earlier turns. Sensitive tools are withheld for the remainder of the session.")
73
+ forbid (
74
+ principal,
75
+ action == Guardrails::Action::"call_tool",
76
+ resource
77
+ )
78
+ when {
79
+ context has session_cumulative_risk_score &&
80
+ context.session_cumulative_risk_score >= 151 &&
81
+ context has tool_is_sensitive && context.tool_is_sensitive == true
82
+ };
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Section 3: Sustained probing, independent of any single score
86
+ // Two is the smallest bar that separates repeated from one-off. Long-running
87
+ // agent sessions accumulate turns faster and warrant a higher bar.
88
+ // ---------------------------------------------------------------------------
89
+
90
+ @id("agent-security.block-tool-on-repeated-threat-turns")
91
+ @name("Block tool use in a session with repeated threat turns")
92
+ @description("Blocks call_tool when session_threat_turns >= 2, because more than one turn in this session tripped a detector.")
93
+ @severity("high")
94
+ @tags("category:agent-security,threat:escalation,detection:aggregate,surface:call-tool,scope:multi-turn,owasp:asi04")
95
+ @reject_message("Tool execution blocked: more than one turn in this session tripped a detector. This is sustained probing, not a one-off false positive.")
96
+ forbid (
97
+ principal,
98
+ action == Guardrails::Action::"call_tool",
99
+ resource
100
+ )
101
+ when {
102
+ context has session_threat_turns && context.session_threat_turns >= 2
103
+ };
@@ -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,112 @@
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
+ ]
602
+ },
603
+ {
604
+ "id": "security.multi-turn-trajectory",
605
+ "name": "Multi-Turn Trajectory Escalation",
606
+ "description": "Block a conversation whose trajectory is an attack even when no single message is: fires on the gap between the multi-turn model and the single-turn classifier.",
607
+ "category": "security",
608
+ "file": "multi_turn_trajectory.cedar",
609
+ "severity": "critical",
610
+ "tags": [
611
+ "category:security",
612
+ "threat:injection",
613
+ "threat:jailbreak",
614
+ "detection:ml",
615
+ "scope:multi-turn",
616
+ "owasp:llm01",
617
+ "owasp:llm02"
618
+ ]
619
+ },
620
+ {
621
+ "id": "agent-security.session-risk-accumulation",
622
+ "name": "Session Risk Accumulation",
623
+ "description": "Block the privileged action a probing conversation is working toward, using the session's high-water mark, cumulative risk, and count of threat turns.",
624
+ "category": "agent-security",
625
+ "file": "session_risk_accumulation.cedar",
626
+ "severity": "critical",
627
+ "tags": [
628
+ "category:agent-security",
629
+ "threat:escalation",
630
+ "detection:aggregate",
631
+ "surface:call-tool",
632
+ "scope:multi-turn",
633
+ "owasp:asi01",
634
+ "owasp:asi04"
635
+ ]
636
+ },
637
+ {
638
+ "id": "agent-identity.dual-attribution",
639
+ "name": "Dual Attribution",
640
+ "description": "Block privileged agent actions that cannot be attributed to a human. Deploy in monitor mode first; a service key is unverified until the agent is adopted.",
641
+ "category": "agent-identity",
642
+ "file": "dual_attribution.cedar",
643
+ "severity": "critical",
644
+ "tags": [
645
+ "category:agent-identity",
646
+ "detection:rule",
647
+ "surface:call-tool",
648
+ "scope:per-agent",
649
+ "posture:deny-default",
650
+ "owasp:asi01"
651
+ ]
506
652
  }
507
653
  ]
508
654
  }
@@ -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
  }
@@ -299,6 +299,47 @@
299
299
  }
300
300
  ]
301
301
  },
302
+ {
303
+ "id": "budget_checker",
304
+ "stability": "stable",
305
+ "tier": "fast",
306
+ "description": "Tracks cumulative session token consumption against a configured budget. On the IDE surface the total is reported by the Overwatch daemon on the prompt and before-tool events (cerberus#147); only the token-budget attributes apply here, not the gateway-metered RPM/TPM ones.",
307
+ "producesAttrs": [
308
+ {
309
+ "name": "budget_remaining_pct",
310
+ "type": "Long",
311
+ "semantic": "severity_0_100",
312
+ "description": "Remaining session token budget (0-100). Default-filled to 100 when no metering ran."
313
+ },
314
+ {
315
+ "name": "budget_exceeded",
316
+ "type": "Bool",
317
+ "semantic": "boolean_flag",
318
+ "description": "True iff the session token budget has been exceeded. Absent when no metering ran, so policies must guard with `context has`."
319
+ }
320
+ ],
321
+ "supportedModes": [
322
+ "enforce",
323
+ "monitor",
324
+ "alert"
325
+ ],
326
+ "modifyContract": null,
327
+ "displayName": "Budget Checker",
328
+ "category": "agent_behavior",
329
+ "defendsAgainst": [
330
+ "unbounded_consumption"
331
+ ],
332
+ "exampleAttacks": [
333
+ {
334
+ "title": "Runaway autonomous session",
335
+ "vulnerabilityId": "unbounded_consumption",
336
+ "snippet": "(agentic) one prompt drives hundreds of tool calls, burning the session's token budget unattended",
337
+ "expectedSignal": {
338
+ "budget_exceeded": true
339
+ }
340
+ }
341
+ ]
342
+ },
302
343
  {
303
344
  "id": "tool_risk",
304
345
  "stability": "stable",
@@ -870,6 +911,12 @@
870
911
  "workspace_root"
871
912
  ],
872
913
  "fieldToDetectorIds": {
914
+ "budget_exceeded": [
915
+ "budget_checker"
916
+ ],
917
+ "budget_remaining_pct": [
918
+ "budget_checker"
919
+ ],
873
920
  "crime_score": [
874
921
  "toxicity"
875
922
  ],
@@ -151,6 +151,24 @@ action process_prompt appliesTo {
151
151
  // --- Legacy ---
152
152
  prompt_text?: String, // Same as content (backward compatibility)
153
153
  response_content?: String, // Response content (if available)
154
+ // --- Session Token Budget --- (OWASP LLM10; CAP-ENF-009, cerberus#147)
155
+ // Fed by the cumulative session total the Overwatch daemon reports on the
156
+ // prompt and before-tool events.
157
+ //
158
+ // BOTH are optional, so BOTH need a `has` guard — a bare
159
+ // `context.budget_remaining_pct < 10` fails cedar validation with
160
+ // "unable to guarantee safety of access to optional attribute".
161
+ // Write: `context has budget_remaining_pct && context.budget_remaining_pct < 10`.
162
+ //
163
+ // The guard is a validation requirement, NOT a safety property. Whenever a
164
+ // session loads, budget_checker emits both keys — including
165
+ // budget_exceeded: false and budget_remaining_pct: 100 when no budget rule
166
+ // is configured for the scope. So a grant conditioned on
167
+ // `!context.budget_exceeded` fires on unbudgeted sessions, and a low
168
+ // percentage is evidence of spend only when a budget rule exists. Absence
169
+ // means the detector did not run at all (no session).
170
+ budget_remaining_pct?: Long, // Remaining session token budget (0-100)
171
+ budget_exceeded?: Bool, // Session token budget exceeded
154
172
  },
155
173
  };
156
174
 
@@ -270,6 +288,24 @@ action call_tool appliesTo {
270
288
 
271
289
  // --- Legacy ---
272
290
  response_content?: String,
291
+ // --- Session Token Budget --- (OWASP LLM10; CAP-ENF-009, cerberus#147)
292
+ // Fed by the cumulative session total the Overwatch daemon reports on the
293
+ // prompt and before-tool events.
294
+ //
295
+ // BOTH are optional, so BOTH need a `has` guard — a bare
296
+ // `context.budget_remaining_pct < 10` fails cedar validation with
297
+ // "unable to guarantee safety of access to optional attribute".
298
+ // Write: `context has budget_remaining_pct && context.budget_remaining_pct < 10`.
299
+ //
300
+ // The guard is a validation requirement, NOT a safety property. Whenever a
301
+ // session loads, budget_checker emits both keys — including
302
+ // budget_exceeded: false and budget_remaining_pct: 100 when no budget rule
303
+ // is configured for the scope. So a grant conditioned on
304
+ // `!context.budget_exceeded` fires on unbudgeted sessions, and a low
305
+ // percentage is evidence of spend only when a budget rule exists. Absence
306
+ // means the detector did not run at all (no session).
307
+ budget_remaining_pct?: Long, // Remaining session token budget (0-100)
308
+ budget_exceeded?: Bool, // Session token budget exceeded
273
309
  },
274
310
  };
275
311
 
@@ -392,6 +428,24 @@ action connect_server appliesTo {
392
428
  session_max_pii_score?: Long,
393
429
  session_max_secret_score?: Long,
394
430
  session_cumulative_risk_score?: Long,
431
+ // --- Session Token Budget --- (OWASP LLM10; CAP-ENF-009, cerberus#147)
432
+ // Fed by the cumulative session total the Overwatch daemon reports on the
433
+ // prompt and before-tool events.
434
+ //
435
+ // BOTH are optional, so BOTH need a `has` guard — a bare
436
+ // `context.budget_remaining_pct < 10` fails cedar validation with
437
+ // "unable to guarantee safety of access to optional attribute".
438
+ // Write: `context has budget_remaining_pct && context.budget_remaining_pct < 10`.
439
+ //
440
+ // The guard is a validation requirement, NOT a safety property. Whenever a
441
+ // session loads, budget_checker emits both keys — including
442
+ // budget_exceeded: false and budget_remaining_pct: 100 when no budget rule
443
+ // is configured for the scope. So a grant conditioned on
444
+ // `!context.budget_exceeded` fires on unbudgeted sessions, and a low
445
+ // percentage is evidence of spend only when a budget rule exists. Absence
446
+ // means the detector did not run at all (no session).
447
+ budget_remaining_pct?: Long, // Remaining session token budget (0-100)
448
+ budget_exceeded?: Bool, // Session token budget exceeded
395
449
  },
396
450
  };
397
451
 
@@ -446,6 +500,24 @@ action read_file appliesTo {
446
500
 
447
501
  // --- Agent Security ---
448
502
  indirect_injection_score?: Long, // Indirect injection via file content
503
+ // --- Session Token Budget --- (OWASP LLM10; CAP-ENF-009, cerberus#147)
504
+ // Fed by the cumulative session total the Overwatch daemon reports on the
505
+ // prompt and before-tool events.
506
+ //
507
+ // BOTH are optional, so BOTH need a `has` guard — a bare
508
+ // `context.budget_remaining_pct < 10` fails cedar validation with
509
+ // "unable to guarantee safety of access to optional attribute".
510
+ // Write: `context has budget_remaining_pct && context.budget_remaining_pct < 10`.
511
+ //
512
+ // The guard is a validation requirement, NOT a safety property. Whenever a
513
+ // session loads, budget_checker emits both keys — including
514
+ // budget_exceeded: false and budget_remaining_pct: 100 when no budget rule
515
+ // is configured for the scope. So a grant conditioned on
516
+ // `!context.budget_exceeded` fires on unbudgeted sessions, and a low
517
+ // percentage is evidence of spend only when a budget rule exists. Absence
518
+ // means the detector did not run at all (no session).
519
+ budget_remaining_pct?: Long, // Remaining session token budget (0-100)
520
+ budget_exceeded?: Bool, // Session token budget exceeded
449
521
  },
450
522
  };
451
523
 
@@ -500,6 +572,24 @@ action write_file appliesTo {
500
572
 
501
573
  // --- Agent Security ---
502
574
  indirect_injection_score?: Long, // Indirect injection via file content
575
+ // --- Session Token Budget --- (OWASP LLM10; CAP-ENF-009, cerberus#147)
576
+ // Fed by the cumulative session total the Overwatch daemon reports on the
577
+ // prompt and before-tool events.
578
+ //
579
+ // BOTH are optional, so BOTH need a `has` guard — a bare
580
+ // `context.budget_remaining_pct < 10` fails cedar validation with
581
+ // "unable to guarantee safety of access to optional attribute".
582
+ // Write: `context has budget_remaining_pct && context.budget_remaining_pct < 10`.
583
+ //
584
+ // The guard is a validation requirement, NOT a safety property. Whenever a
585
+ // session loads, budget_checker emits both keys — including
586
+ // budget_exceeded: false and budget_remaining_pct: 100 when no budget rule
587
+ // is configured for the scope. So a grant conditioned on
588
+ // `!context.budget_exceeded` fires on unbudgeted sessions, and a low
589
+ // percentage is evidence of spend only when a budget rule exists. Absence
590
+ // means the detector did not run at all (no session).
591
+ budget_remaining_pct?: Long, // Remaining session token budget (0-100)
592
+ budget_exceeded?: Bool, // Session token budget exceeded
503
593
  },
504
594
  };
505
595