consequence-gate 0.1.0__tar.gz

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 (37) hide show
  1. consequence_gate-0.1.0/PKG-INFO +254 -0
  2. consequence_gate-0.1.0/README.md +218 -0
  3. consequence_gate-0.1.0/consequence_gate/__init__.py +3 -0
  4. consequence_gate-0.1.0/consequence_gate/backtest/__init__.py +0 -0
  5. consequence_gate-0.1.0/consequence_gate/backtest/harness.py +43 -0
  6. consequence_gate-0.1.0/consequence_gate/backtest/reporter.py +20 -0
  7. consequence_gate-0.1.0/consequence_gate/cli.py +75 -0
  8. consequence_gate-0.1.0/consequence_gate/core/__init__.py +0 -0
  9. consequence_gate-0.1.0/consequence_gate/core/circuit_breaker.py +56 -0
  10. consequence_gate-0.1.0/consequence_gate/core/evaluator.py +27 -0
  11. consequence_gate-0.1.0/consequence_gate/core/models.py +35 -0
  12. consequence_gate-0.1.0/consequence_gate/integrations/__init__.py +0 -0
  13. consequence_gate-0.1.0/consequence_gate/integrations/examples/run_langgraph.py +60 -0
  14. consequence_gate-0.1.0/consequence_gate/integrations/examples/run_mcp_proxy.py +100 -0
  15. consequence_gate-0.1.0/consequence_gate/integrations/langgraph_hook.py +197 -0
  16. consequence_gate-0.1.0/consequence_gate/integrations/mcp_proxy.py +316 -0
  17. consequence_gate-0.1.0/consequence_gate/integrations/strands_hook.py +224 -0
  18. consequence_gate-0.1.0/consequence_gate/simulators/__init__.py +0 -0
  19. consequence_gate-0.1.0/consequence_gate/simulators/communications.py +290 -0
  20. consequence_gate-0.1.0/consequence_gate/simulators/database.py +165 -0
  21. consequence_gate-0.1.0/consequence_gate/simulators/financial.py +102 -0
  22. consequence_gate-0.1.0/consequence_gate.egg-info/PKG-INFO +254 -0
  23. consequence_gate-0.1.0/consequence_gate.egg-info/SOURCES.txt +35 -0
  24. consequence_gate-0.1.0/consequence_gate.egg-info/dependency_links.txt +1 -0
  25. consequence_gate-0.1.0/consequence_gate.egg-info/entry_points.txt +2 -0
  26. consequence_gate-0.1.0/consequence_gate.egg-info/requires.txt +16 -0
  27. consequence_gate-0.1.0/consequence_gate.egg-info/top_level.txt +1 -0
  28. consequence_gate-0.1.0/pyproject.toml +86 -0
  29. consequence_gate-0.1.0/setup.cfg +4 -0
  30. consequence_gate-0.1.0/tests/test_circuit_breaker.py +23 -0
  31. consequence_gate-0.1.0/tests/test_cli.py +34 -0
  32. consequence_gate-0.1.0/tests/test_communications_sim.py +212 -0
  33. consequence_gate-0.1.0/tests/test_database_sim.py +21 -0
  34. consequence_gate-0.1.0/tests/test_financial_sim.py +35 -0
  35. consequence_gate-0.1.0/tests/test_langgraph_hook.py +167 -0
  36. consequence_gate-0.1.0/tests/test_mcp_proxy.py +198 -0
  37. consequence_gate-0.1.0/tests/test_strands_hook.py +260 -0
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.4
2
+ Name: consequence-gate
3
+ Version: 0.1.0
4
+ Summary: Speculative outcome-simulation layer for AI agent tool calls — predicts consequence (blast radius, irreversibility, velocity) before execution and steers agents toward safer alternatives.
5
+ Author-email: Anandkrishnan Shnn <anandkrshnn@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/anandkrshnn-ai/consequence-gate
8
+ Project-URL: Documentation, https://github.com/anandkrshnn-ai/consequence-gate#readme
9
+ Project-URL: Repository, https://github.com/anandkrshnn-ai/consequence-gate
10
+ Project-URL: Issues, https://github.com/anandkrshnn-ai/consequence-gate/issues
11
+ Keywords: ai-agents,ai-safety,ai-governance,langchain,langgraph,mcp,agent-security,runtime-governance
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: langchain>=0.3.0
25
+ Requires-Dist: langchain-agents>=0.3.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
29
+ Requires-Dist: black>=23.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
31
+ Provides-Extra: strands
32
+ Requires-Dist: strands-agents>=0.1.0; extra == "strands"
33
+ Provides-Extra: mcp
34
+ Provides-Extra: langgraph
35
+ Requires-Dist: langgraph>=0.2.0; extra == "langgraph"
36
+
37
+ # consequence-gate
38
+
39
+ A speculative outcome-simulation layer for AI agent tool calls. It sits
40
+ **upstream** of static runtime access gates (AgentWall, AWS Strands
41
+ `BeforeToolCallEvent`, MCP proxies, Prisma AIRS) and asks a different
42
+ question than they do.
43
+
44
+ Static gates ask: *does this call match an allowed pattern?*
45
+ `consequence-gate` asks: *what will this call actually do, and is that
46
+ outcome safe?*
47
+
48
+ ## Why this exists
49
+
50
+ Static runtime gates are fast (sub-millisecond) and effective at schema
51
+ validation, RBAC, and pattern matching -- but a schema-valid,
52
+ policy-compliant call can still be consequence-catastrophic. A
53
+ `process_claim(amount=50000)` call can pass every static check while
54
+ pushing an account over its daily velocity limit via an irreversible
55
+ instant transfer. `consequence-gate` projects the *outcome* of a call
56
+ (balance deltas, row-count blast radius, FK cascade depth,
57
+ irreversibility) before the call reaches your existing static gate, and
58
+ either passes it through, asks a human, denies it outright, or steers
59
+ the agent toward a pre-vetted safer alternative.
60
+
61
+ This is explicitly **not** a replacement for AgentWall / Strands / MCP
62
+ proxies -- it's a prediction layer that runs before them, in the same
63
+ pipeline.
64
+
65
+ ## Core contracts
66
+
67
+ - **No silent argument mutation.** Steering returns structured guidance
68
+ and a suggested alternative call; the agent (or a human) still has to
69
+ commit to it. This preserves the audit property that every executed
70
+ call was one the agent explicitly chose.
71
+ - **Idempotency keys are derived from the transaction's own natural key**
72
+ (e.g. `claim_id`, or `table + filter hash`), never a fresh random token
73
+ per retry -- otherwise a lost-response retry looks like a brand-new
74
+ transaction instead of a duplicate.
75
+ - **Hard retry cap on STEER.** Regardless of guidance quality, retries
76
+ are capped (default: 2) before forcing escalation to a human, as a
77
+ backstop against loop-thrashing.
78
+ - **Confidence-gated escalation.** Low-confidence projections route to
79
+ `ASK`, never to a confident-looking `ALLOW` or `DENY` -- an
80
+ unfounded heuristic is worse than admitting uncertainty.
81
+
82
+ ## Modules
83
+
84
+ - `consequence_gate.simulators.financial` -- **functional**: disbursement / claim / refund
85
+ velocity and irreversibility modeling.
86
+ - `consequence_gate.simulators.database` -- **functional**: row-count blast radius via the
87
+ DB's own query planner (`EXPLAIN`, not hardcoded selectivity constants)
88
+ and recursive `ON DELETE CASCADE` graph walking.
89
+ - `consequence_gate.simulators.communications` -- **functional**: outbound email/SMS/notification
90
+ blast radius, unsubscribe suppression compliance, canary cohort analysis, sender reputation impact.
91
+ - `consequence_gate.core` -- shared models, the confidence/threshold
92
+ evaluator, and the idempotency-locked circuit breaker.
93
+ - `consequence_gate.integrations.strands_hook` -- **functional**: AWS Strands
94
+ `BeforeToolCallEvent` adapter with full `ALLOW`/`DENY`/`ASK`/`STEER` lifecycle.
95
+ - `consequence_gate.integrations.mcp_proxy` -- **functional**: MCP stdio proxy
96
+ intercepting `tools/call` requests, returning JSON-RPC errors or `isError=true` tool results.
97
+ - `consequence_gate.integrations.langgraph_hook` -- **functional**: LangGraph middleware
98
+ (`@wrap_tool_call`) intercepting tool execution with full `ALLOW`/`DENY`/`ASK`/`STEER` lifecycle.
99
+ - `consequence_gate.backtest` -- offline JSONL trace replay harness and
100
+ four-quadrant FP/FN/TN report generator, for evaluating this layer
101
+ against historical execution logs with zero production integration.
102
+
103
+ ## Quickstart
104
+
105
+ ### Installation
106
+
107
+ ```bash
108
+ pip install -e ".[dev]"
109
+ pytest
110
+ python examples/run_backtest_demo.py
111
+ ```
112
+
113
+ ### AWS Strands Integration
114
+
115
+ ```python
116
+ from consequence_gate.integrations.strands_hook import create_financial_gate_hook
117
+ from strands.agents import Agent
118
+
119
+ hook = create_financial_gate_hook(
120
+ daily_tier_limit_inr=25000.0,
121
+ instant_wire_threshold=10000.0,
122
+ max_retries=2,
123
+ context_provider=lambda event: {
124
+ "account_rolling_24h_spend": get_current_spend(event),
125
+ "kyc_verified": is_kyc_verified(event),
126
+ },
127
+ )
128
+
129
+ agent = Agent(hooks=[hook])
130
+ response = agent("Process this claim for 50,000 INR")
131
+ ```
132
+
133
+ ### MCP Proxy Integration
134
+
135
+ Run as a standalone proxy in front of any MCP server:
136
+
137
+ ```bash
138
+ # Financial disbursement gate
139
+ python -m consequence_gate.integrations.examples.run_mcp_proxy financial \\
140
+ --downstream-command "npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb" \\
141
+ --daily-tier-limit 25000 \\
142
+ --instant-wire-threshold 10000
143
+ ```
144
+
145
+ Configure in Claude Desktop / Cursor / Windsurf:
146
+
147
+ ```json
148
+ {
149
+ "mcpServers": {
150
+ "my-consequence-gate": {
151
+ "command": "python",
152
+ "args": ["-m", "consequence_gate.integrations.examples.run_mcp_proxy", "financial", "--downstream-command", "npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb"]
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### LangGraph Integration
159
+
160
+ ```python
161
+ from langchain.agents import create_agent
162
+ from consequence_gate.integrations.langgraph_hook import create_financial_gate_middleware
163
+
164
+ middleware = create_financial_gate_middleware(
165
+ daily_tier_limit_inr=25000.0,
166
+ instant_wire_threshold=10000.0,
167
+ max_retries=2,
168
+ )
169
+
170
+ agent = create_agent(
171
+ model="claude-sonnet-4",
172
+ tools=[my_tool],
173
+ middleware=[middleware],
174
+ )
175
+ ```
176
+
177
+ Run the example:
178
+
179
+ ```bash
180
+ python -m consequence_gate.integrations.examples.run_langgraph
181
+ ```
182
+
183
+ ### Database Deletion Gate (Strands)
184
+
185
+ ```python
186
+ from consequence_gate.integrations.strands_hook import create_database_gate_hook
187
+
188
+ hook = create_database_gate_hook(
189
+ max_autonomous_delete_rows=100,
190
+ db_conn=get_db_connection(),
191
+ max_retries=2,
192
+ context_provider=lambda event: {
193
+ "table_metadata": get_table_metadata(event),
194
+ },
195
+ )
196
+
197
+ agent = Agent(hooks=[hook])
198
+ ```
199
+
200
+ ### Communications Blast Gate (Strands)
201
+
202
+ ```python
203
+ from consequence_gate.integrations.strands_hook import create_communication_gate_hook
204
+
205
+ hook = create_communication_gate_hook(
206
+ max_autonomous_recipients=10000,
207
+ canary_min_size=100,
208
+ canary_max_bounce_rate=0.05,
209
+ canary_max_complaint_rate=0.01,
210
+ context_provider=lambda event: {
211
+ "segment_counts": get_segment_counts(event),
212
+ "recent_unsubscribes": get_recent_unsubscribes(event),
213
+ "historical_bounce_rate": 0.02,
214
+ "historical_complaint_rate": 0.005,
215
+ },
216
+ )
217
+
218
+ agent = Agent(hooks=[hook])
219
+ ```
220
+
221
+ ## Decision Matrix
222
+
223
+ | Decision | Strands | MCP | LangGraph |
224
+ |----------|---------|-----|-----------|
225
+ | `ALLOW` | Executes normally | Forwarded to downstream MCP server | Tool executes via handler(request) |
226
+ | `DENY` | `BLOCKED: <reason>` | JSON-RPC error (code=-32603) | Raises `ValueError("BLOCKED: ...")` |
227
+ | `ASK` | `ESCALATION_REQUIRED: <reason>` | `isError=true` tool result | Raises `ValueError("ESCALATION_REQUIRED: ...")` |
228
+ | `STEER` | `STEER_GUIDANCE: <guidance>\nSuggested alternative...` | `isError=true` + guidance | `ToolMessage(content="STEER_GUIDANCE: ...", status="error")` |
229
+
230
+ ## Backtest Workflow
231
+
232
+ Before deploying to production, run an offline backtest against historical
233
+ execution traces:
234
+
235
+ 1. Export 1,000-5,000 tool-call traces as JSONL (see `examples/backtest_sample_traces.jsonl`)
236
+ 2. Run `python examples/run_backtest_demo.py` against your traces
237
+ 3. Review the four-quadrant breakdown:
238
+ - True Negative: correctly allowed benign operations
239
+ - False Negative Caught: schema-valid calls that would have breached limits
240
+ - False Positive Relieved: over-blocking that the simulator would have avoided
241
+ - Steer Recovery Rate: percentage of blocked turns that could have completed via guidance
242
+
243
+ ## Status
244
+
245
+ - **Financial simulator**: functional with unit tests
246
+ - **Database simulator**: functional with unit tests (EXPLAIN-based row estimation, recursive FK cascade walk)
247
+ - **Communications simulator**: functional with unit tests (blast radius, unsubscribe compliance, canary cohorts, reputation impact)
248
+ - **Strands integration**: functional with unit tests (full `ALLOW`/`DENY`/`ASK`/`STEER` lifecycle)
249
+ - **MCP integration**: functional with unit tests (stdio transport, JSON-RPC error handling)
250
+ - **LangGraph integration**: functional with unit tests (`@wrap_tool_call` middleware pattern)
251
+
252
+ ## License
253
+
254
+ Apache-2.0
@@ -0,0 +1,218 @@
1
+ # consequence-gate
2
+
3
+ A speculative outcome-simulation layer for AI agent tool calls. It sits
4
+ **upstream** of static runtime access gates (AgentWall, AWS Strands
5
+ `BeforeToolCallEvent`, MCP proxies, Prisma AIRS) and asks a different
6
+ question than they do.
7
+
8
+ Static gates ask: *does this call match an allowed pattern?*
9
+ `consequence-gate` asks: *what will this call actually do, and is that
10
+ outcome safe?*
11
+
12
+ ## Why this exists
13
+
14
+ Static runtime gates are fast (sub-millisecond) and effective at schema
15
+ validation, RBAC, and pattern matching -- but a schema-valid,
16
+ policy-compliant call can still be consequence-catastrophic. A
17
+ `process_claim(amount=50000)` call can pass every static check while
18
+ pushing an account over its daily velocity limit via an irreversible
19
+ instant transfer. `consequence-gate` projects the *outcome* of a call
20
+ (balance deltas, row-count blast radius, FK cascade depth,
21
+ irreversibility) before the call reaches your existing static gate, and
22
+ either passes it through, asks a human, denies it outright, or steers
23
+ the agent toward a pre-vetted safer alternative.
24
+
25
+ This is explicitly **not** a replacement for AgentWall / Strands / MCP
26
+ proxies -- it's a prediction layer that runs before them, in the same
27
+ pipeline.
28
+
29
+ ## Core contracts
30
+
31
+ - **No silent argument mutation.** Steering returns structured guidance
32
+ and a suggested alternative call; the agent (or a human) still has to
33
+ commit to it. This preserves the audit property that every executed
34
+ call was one the agent explicitly chose.
35
+ - **Idempotency keys are derived from the transaction's own natural key**
36
+ (e.g. `claim_id`, or `table + filter hash`), never a fresh random token
37
+ per retry -- otherwise a lost-response retry looks like a brand-new
38
+ transaction instead of a duplicate.
39
+ - **Hard retry cap on STEER.** Regardless of guidance quality, retries
40
+ are capped (default: 2) before forcing escalation to a human, as a
41
+ backstop against loop-thrashing.
42
+ - **Confidence-gated escalation.** Low-confidence projections route to
43
+ `ASK`, never to a confident-looking `ALLOW` or `DENY` -- an
44
+ unfounded heuristic is worse than admitting uncertainty.
45
+
46
+ ## Modules
47
+
48
+ - `consequence_gate.simulators.financial` -- **functional**: disbursement / claim / refund
49
+ velocity and irreversibility modeling.
50
+ - `consequence_gate.simulators.database` -- **functional**: row-count blast radius via the
51
+ DB's own query planner (`EXPLAIN`, not hardcoded selectivity constants)
52
+ and recursive `ON DELETE CASCADE` graph walking.
53
+ - `consequence_gate.simulators.communications` -- **functional**: outbound email/SMS/notification
54
+ blast radius, unsubscribe suppression compliance, canary cohort analysis, sender reputation impact.
55
+ - `consequence_gate.core` -- shared models, the confidence/threshold
56
+ evaluator, and the idempotency-locked circuit breaker.
57
+ - `consequence_gate.integrations.strands_hook` -- **functional**: AWS Strands
58
+ `BeforeToolCallEvent` adapter with full `ALLOW`/`DENY`/`ASK`/`STEER` lifecycle.
59
+ - `consequence_gate.integrations.mcp_proxy` -- **functional**: MCP stdio proxy
60
+ intercepting `tools/call` requests, returning JSON-RPC errors or `isError=true` tool results.
61
+ - `consequence_gate.integrations.langgraph_hook` -- **functional**: LangGraph middleware
62
+ (`@wrap_tool_call`) intercepting tool execution with full `ALLOW`/`DENY`/`ASK`/`STEER` lifecycle.
63
+ - `consequence_gate.backtest` -- offline JSONL trace replay harness and
64
+ four-quadrant FP/FN/TN report generator, for evaluating this layer
65
+ against historical execution logs with zero production integration.
66
+
67
+ ## Quickstart
68
+
69
+ ### Installation
70
+
71
+ ```bash
72
+ pip install -e ".[dev]"
73
+ pytest
74
+ python examples/run_backtest_demo.py
75
+ ```
76
+
77
+ ### AWS Strands Integration
78
+
79
+ ```python
80
+ from consequence_gate.integrations.strands_hook import create_financial_gate_hook
81
+ from strands.agents import Agent
82
+
83
+ hook = create_financial_gate_hook(
84
+ daily_tier_limit_inr=25000.0,
85
+ instant_wire_threshold=10000.0,
86
+ max_retries=2,
87
+ context_provider=lambda event: {
88
+ "account_rolling_24h_spend": get_current_spend(event),
89
+ "kyc_verified": is_kyc_verified(event),
90
+ },
91
+ )
92
+
93
+ agent = Agent(hooks=[hook])
94
+ response = agent("Process this claim for 50,000 INR")
95
+ ```
96
+
97
+ ### MCP Proxy Integration
98
+
99
+ Run as a standalone proxy in front of any MCP server:
100
+
101
+ ```bash
102
+ # Financial disbursement gate
103
+ python -m consequence_gate.integrations.examples.run_mcp_proxy financial \\
104
+ --downstream-command "npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb" \\
105
+ --daily-tier-limit 25000 \\
106
+ --instant-wire-threshold 10000
107
+ ```
108
+
109
+ Configure in Claude Desktop / Cursor / Windsurf:
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "my-consequence-gate": {
115
+ "command": "python",
116
+ "args": ["-m", "consequence_gate.integrations.examples.run_mcp_proxy", "financial", "--downstream-command", "npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb"]
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### LangGraph Integration
123
+
124
+ ```python
125
+ from langchain.agents import create_agent
126
+ from consequence_gate.integrations.langgraph_hook import create_financial_gate_middleware
127
+
128
+ middleware = create_financial_gate_middleware(
129
+ daily_tier_limit_inr=25000.0,
130
+ instant_wire_threshold=10000.0,
131
+ max_retries=2,
132
+ )
133
+
134
+ agent = create_agent(
135
+ model="claude-sonnet-4",
136
+ tools=[my_tool],
137
+ middleware=[middleware],
138
+ )
139
+ ```
140
+
141
+ Run the example:
142
+
143
+ ```bash
144
+ python -m consequence_gate.integrations.examples.run_langgraph
145
+ ```
146
+
147
+ ### Database Deletion Gate (Strands)
148
+
149
+ ```python
150
+ from consequence_gate.integrations.strands_hook import create_database_gate_hook
151
+
152
+ hook = create_database_gate_hook(
153
+ max_autonomous_delete_rows=100,
154
+ db_conn=get_db_connection(),
155
+ max_retries=2,
156
+ context_provider=lambda event: {
157
+ "table_metadata": get_table_metadata(event),
158
+ },
159
+ )
160
+
161
+ agent = Agent(hooks=[hook])
162
+ ```
163
+
164
+ ### Communications Blast Gate (Strands)
165
+
166
+ ```python
167
+ from consequence_gate.integrations.strands_hook import create_communication_gate_hook
168
+
169
+ hook = create_communication_gate_hook(
170
+ max_autonomous_recipients=10000,
171
+ canary_min_size=100,
172
+ canary_max_bounce_rate=0.05,
173
+ canary_max_complaint_rate=0.01,
174
+ context_provider=lambda event: {
175
+ "segment_counts": get_segment_counts(event),
176
+ "recent_unsubscribes": get_recent_unsubscribes(event),
177
+ "historical_bounce_rate": 0.02,
178
+ "historical_complaint_rate": 0.005,
179
+ },
180
+ )
181
+
182
+ agent = Agent(hooks=[hook])
183
+ ```
184
+
185
+ ## Decision Matrix
186
+
187
+ | Decision | Strands | MCP | LangGraph |
188
+ |----------|---------|-----|-----------|
189
+ | `ALLOW` | Executes normally | Forwarded to downstream MCP server | Tool executes via handler(request) |
190
+ | `DENY` | `BLOCKED: <reason>` | JSON-RPC error (code=-32603) | Raises `ValueError("BLOCKED: ...")` |
191
+ | `ASK` | `ESCALATION_REQUIRED: <reason>` | `isError=true` tool result | Raises `ValueError("ESCALATION_REQUIRED: ...")` |
192
+ | `STEER` | `STEER_GUIDANCE: <guidance>\nSuggested alternative...` | `isError=true` + guidance | `ToolMessage(content="STEER_GUIDANCE: ...", status="error")` |
193
+
194
+ ## Backtest Workflow
195
+
196
+ Before deploying to production, run an offline backtest against historical
197
+ execution traces:
198
+
199
+ 1. Export 1,000-5,000 tool-call traces as JSONL (see `examples/backtest_sample_traces.jsonl`)
200
+ 2. Run `python examples/run_backtest_demo.py` against your traces
201
+ 3. Review the four-quadrant breakdown:
202
+ - True Negative: correctly allowed benign operations
203
+ - False Negative Caught: schema-valid calls that would have breached limits
204
+ - False Positive Relieved: over-blocking that the simulator would have avoided
205
+ - Steer Recovery Rate: percentage of blocked turns that could have completed via guidance
206
+
207
+ ## Status
208
+
209
+ - **Financial simulator**: functional with unit tests
210
+ - **Database simulator**: functional with unit tests (EXPLAIN-based row estimation, recursive FK cascade walk)
211
+ - **Communications simulator**: functional with unit tests (blast radius, unsubscribe compliance, canary cohorts, reputation impact)
212
+ - **Strands integration**: functional with unit tests (full `ALLOW`/`DENY`/`ASK`/`STEER` lifecycle)
213
+ - **MCP integration**: functional with unit tests (stdio transport, JSON-RPC error handling)
214
+ - **LangGraph integration**: functional with unit tests (`@wrap_tool_call` middleware pattern)
215
+
216
+ ## License
217
+
218
+ Apache-2.0
@@ -0,0 +1,3 @@
1
+ """consequence-gate: speculative outcome-simulation layer for AI agent tool calls."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,43 @@
1
+ """
2
+ Offline backtest harness: replays historical JSONL tool-call traces
3
+ through a simulator + evaluator, WITHOUT re-executing anything, to
4
+ measure the four-quadrant FP/FN/TN/steer-recovery breakdown against
5
+ the trace's recorded existing_gate_decision and actual_execution_status.
6
+ """
7
+
8
+ import json
9
+ from typing import Callable, Dict, Iterable, List
10
+
11
+
12
+ def load_traces(path: str) -> List[Dict]:
13
+ traces = []
14
+ with open(path) as f:
15
+ for line in f:
16
+ line = line.strip()
17
+ if line:
18
+ traces.append(json.loads(line))
19
+ return traces
20
+
21
+
22
+ def run_backtest(traces: Iterable[Dict], simulate_and_evaluate: Callable[[Dict], str]) -> List[Dict]:
23
+ """
24
+ simulate_and_evaluate: function(trace) -> decision string ("ALLOW"/"DENY"/"ASK"/"STEER")
25
+ Returns per-trace records annotated with quadrant classification.
26
+ """
27
+ results = []
28
+ for trace in traces:
29
+ new_decision = simulate_and_evaluate(trace)
30
+ old_decision = trace.get("existing_gate_decision", "ALLOW")
31
+ outcome = trace.get("actual_execution_status", "UNKNOWN")
32
+
33
+ if old_decision == "ALLOW" and new_decision in ("DENY", "STEER", "ASK") and outcome != "SUCCESS":
34
+ quadrant = "FALSE_NEGATIVE_CAUGHT"
35
+ elif old_decision in ("DENY", "ASK") and new_decision == "ALLOW":
36
+ quadrant = "FALSE_POSITIVE_RELIEVED"
37
+ elif old_decision == "ALLOW" and new_decision == "ALLOW":
38
+ quadrant = "TRUE_NEGATIVE"
39
+ else:
40
+ quadrant = "OTHER"
41
+
42
+ results.append({**trace, "simulated_decision": new_decision, "quadrant": quadrant})
43
+ return results
@@ -0,0 +1,20 @@
1
+ """
2
+ Generates the four-quadrant FP/FN breakdown report from backtest results.
3
+ """
4
+
5
+ from collections import Counter
6
+ from typing import Dict, List
7
+
8
+
9
+ def generate_report(results: List[Dict]) -> Dict:
10
+ counts = Counter(r["quadrant"] for r in results)
11
+ total = len(results)
12
+ return {
13
+ "total_traces": total,
14
+ "true_negative": counts.get("TRUE_NEGATIVE", 0),
15
+ "false_negative_caught": counts.get("FALSE_NEGATIVE_CAUGHT", 0),
16
+ "false_positive_relieved": counts.get("FALSE_POSITIVE_RELIEVED", 0),
17
+ "other": counts.get("OTHER", 0),
18
+ "false_negative_rate": counts.get("FALSE_NEGATIVE_CAUGHT", 0) / total if total else 0.0,
19
+ "false_positive_relief_rate": counts.get("FALSE_POSITIVE_RELIEVED", 0) / total if total else 0.0,
20
+ }
@@ -0,0 +1,75 @@
1
+ """
2
+ CLI entry point for consequence-gate.
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from consequence_gate import __version__
9
+ from consequence_gate.backtest.harness import load_traces, run_backtest
10
+ from consequence_gate.backtest.reporter import generate_report
11
+
12
+
13
+ def default_evaluator(trace: dict) -> str:
14
+ """Heuristic evaluator for CLI demo and trace analysis."""
15
+ tool = trace.get("tool_name", "")
16
+ args = trace.get("tool_args", {})
17
+
18
+ tool_lower = tool.lower()
19
+ args_str = str(args).lower()
20
+
21
+ if any(k in tool_lower for k in ("delete", "drop", "purge", "truncate")) or any(
22
+ k in args_str for k in ("drop ", "delete from", "truncate ", "purge")
23
+ ):
24
+ return "DENY"
25
+ if "transfer" in tool.lower() or "pay" in tool.lower():
26
+ amount = args.get("amount", 0)
27
+ if isinstance(amount, (int, float)) and amount > 5000:
28
+ return "ASK"
29
+ return "ALLOW"
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(
34
+ prog="consequence-gate",
35
+ description="Speculative outcome-simulation gate & trace backtesting for AI agent tool calls.",
36
+ )
37
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
38
+
39
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
40
+
41
+ # Backtest subcommand
42
+ bt_parser = subparsers.add_parser("backtest", help="Run offline backtesting on a JSONL trace file")
43
+ bt_parser.add_argument("file", help="Path to JSONL file containing recorded agent traces")
44
+ bt_parser.add_argument("--json", action="store_true", help="Output results in JSON format")
45
+
46
+ args = parser.parse_args()
47
+
48
+ if args.command == "backtest":
49
+ try:
50
+ traces = load_traces(args.file)
51
+ results = run_backtest(traces, default_evaluator)
52
+ report = generate_report(results)
53
+
54
+ if args.json:
55
+ print(json.dumps(report, indent=2))
56
+ else:
57
+ print("\n================ CONSEQUENCE GATE BACKTEST REPORT ================")
58
+ print(f"Total Traces Evaluated: {report['total_traces']}")
59
+ print(f"True Negatives: {report['true_negative']}")
60
+ print(f"False Negatives Caught: {report['false_negative_caught']}")
61
+ print(f"False Positives Relieved: {report['false_positive_relieved']}")
62
+ print(f"Other / Unclassified: {report['other']}")
63
+ print("-----------------------------------------------------------------")
64
+ print(f"False Negative Catch Rate: {report['false_negative_rate']:.2%}")
65
+ print(f"False Positive Relief Rate: {report['false_positive_relief_rate']:.2%}")
66
+ print("=================================================================\n")
67
+ except Exception as e:
68
+ print(f"Error executing backtest: {e}", file=sys.stderr)
69
+ sys.exit(1)
70
+ else:
71
+ parser.print_help()
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,56 @@
1
+ """
2
+ SteerCircuitBreaker: idempotency-locked retry cap for STEER decisions.
3
+
4
+ Design contract (see project history / design notes):
5
+ - The idempotency token is derived ONCE from the transaction's own natural
6
+ key (e.g. claim_id, table+filter hash) -- never regenerated per retry.
7
+ A fresh UUID per attempt defeats duplicate-execution protection.
8
+ - Responses are cached per token, so a retry with the same natural key
9
+ returns the cached result instead of re-executing (Stripe-style contract).
10
+ - Retry count is tracked server-side per token, with a hard cap. Once
11
+ exceeded, the breaker forces ASK (human escalation) regardless of how
12
+ good the steering guidance is -- this is a backstop against
13
+ loop-thrashing, independent of guidance quality.
14
+ """
15
+
16
+ from typing import Any, Dict
17
+ from .models import GateDecision, EvaluationResult
18
+
19
+
20
+ class SteerCircuitBreaker:
21
+ def __init__(self, max_retries: int = 2):
22
+ self.max_retries = max_retries
23
+ self._attempts: Dict[str, int] = {}
24
+ self._responses: Dict[str, EvaluationResult] = {}
25
+
26
+ def token_for(self, natural_key: str) -> str:
27
+ return f"steer_{natural_key}"
28
+
29
+ def resolve(self, natural_key: str, confidence: float,
30
+ base_steer: Dict[str, Any]) -> EvaluationResult:
31
+ token = self.token_for(natural_key)
32
+
33
+ if token in self._responses:
34
+ return self._responses[token]
35
+
36
+ attempt = self._attempts.get(token, 0)
37
+
38
+ if attempt >= self.max_retries:
39
+ result = EvaluationResult(
40
+ decision=GateDecision.ASK,
41
+ confidence=confidence,
42
+ reason=f"Steer circuit breaker tripped ({attempt}/{self.max_retries}). Escalating to human.",
43
+ )
44
+ self._responses[token] = result
45
+ return result
46
+
47
+ self._attempts[token] = attempt + 1
48
+ base_steer.setdefault("suggested_args", {})["idempotency_key"] = token
49
+
50
+ result = EvaluationResult(
51
+ decision=GateDecision.STEER,
52
+ confidence=confidence,
53
+ reason=f"Steered to safer path (attempt {attempt + 1}/{self.max_retries}).",
54
+ steer_payload=base_steer,
55
+ )
56
+ return result