auditable 0.1.0__tar.gz → 0.2.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 (82) hide show
  1. {auditable-0.1.0 → auditable-0.2.0}/.gitignore +4 -0
  2. auditable-0.2.0/PKG-INFO +389 -0
  3. auditable-0.2.0/README.md +342 -0
  4. auditable-0.2.0/examples/README.md +226 -0
  5. auditable-0.1.0/examples/audit_report.py → auditable-0.2.0/examples/example_audit_report.py +1 -1
  6. auditable-0.1.0/examples/end_to_end.py → auditable-0.2.0/examples/example_end_to_end.py +7 -6
  7. auditable-0.2.0/examples/example_langchain_capture.py +100 -0
  8. auditable-0.2.0/examples/example_langgraph_capture.py +105 -0
  9. auditable-0.2.0/examples/example_langgraph_live_replay.py +154 -0
  10. auditable-0.2.0/examples/example_langgraph_llm_agent.py +173 -0
  11. auditable-0.2.0/examples/example_live_monitor.py +78 -0
  12. auditable-0.2.0/examples/example_live_replay.py +74 -0
  13. auditable-0.2.0/examples/example_mcp_capture.py +116 -0
  14. auditable-0.1.0/examples/payment_audit.py → auditable-0.2.0/examples/example_payment_audit.py +113 -113
  15. auditable-0.1.0/examples/analyze_run.py → auditable-0.2.0/examples/example_post_rank_run.py +99 -85
  16. auditable-0.1.0/examples/analyze_plan.py → auditable-0.2.0/examples/example_pre_lint_plan.py +17 -6
  17. auditable-0.1.0/examples/standalone_report.py → auditable-0.2.0/examples/example_standalone_report.py +4 -4
  18. auditable-0.2.0/examples/example_state_anomaly.py +123 -0
  19. auditable-0.2.0/examples/example_touch_capture.py +73 -0
  20. {auditable-0.1.0 → auditable-0.2.0}/pyproject.toml +21 -4
  21. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/__init__.py +79 -77
  22. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/analysis.py +615 -369
  23. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/data/__init__.py +35 -6
  24. auditable-0.2.0/src/auditable/data/inject.py +225 -0
  25. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/__init__.py +6 -4
  26. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/adapters/__init__.py +44 -44
  27. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/adapters/own_record.py +142 -142
  28. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/adapters/protocol.py +64 -64
  29. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/adapters/tau_bench.py +138 -138
  30. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/grounding.py +332 -332
  31. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/loader.py +250 -250
  32. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/risk.py +128 -128
  33. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/session.py +292 -292
  34. auditable-0.2.0/src/auditable/graph/touch.py +350 -0
  35. auditable-0.2.0/src/auditable/integrations/__init__.py +10 -0
  36. auditable-0.2.0/src/auditable/integrations/langchain.py +805 -0
  37. auditable-0.2.0/src/auditable/integrations/langgraph.py +727 -0
  38. auditable-0.2.0/src/auditable/integrations/mcp.py +328 -0
  39. {auditable-0.1.0 → auditable-0.2.0}/tests/test_adapters.py +352 -352
  40. {auditable-0.1.0 → auditable-0.2.0}/tests/test_analysis.py +188 -188
  41. auditable-0.2.0/tests/test_data_inject.py +194 -0
  42. {auditable-0.1.0 → auditable-0.2.0}/tests/test_data_module.py +61 -1
  43. {auditable-0.1.0 → auditable-0.2.0}/tests/test_graph_loader.py +293 -293
  44. {auditable-0.1.0 → auditable-0.2.0}/tests/test_graph_risk.py +134 -134
  45. {auditable-0.1.0 → auditable-0.2.0}/tests/test_grounding.py +249 -249
  46. auditable-0.2.0/tests/test_langchain_integration.py +292 -0
  47. auditable-0.2.0/tests/test_langgraph_integration.py +590 -0
  48. auditable-0.2.0/tests/test_langgraph_replay_bridge.py +132 -0
  49. auditable-0.2.0/tests/test_live_session.py +372 -0
  50. auditable-0.2.0/tests/test_mcp_integration.py +264 -0
  51. {auditable-0.1.0 → auditable-0.2.0}/tests/test_public_api.py +99 -92
  52. {auditable-0.1.0 → auditable-0.2.0}/tests/test_session_graph.py +157 -157
  53. {auditable-0.1.0 → auditable-0.2.0}/tests/test_session_graph_projection.py +141 -141
  54. auditable-0.2.0/tests/test_touch.py +205 -0
  55. auditable-0.2.0/tests/test_touch_replay_bridge.py +79 -0
  56. auditable-0.1.0/PKG-INFO +0 -289
  57. auditable-0.1.0/README.md +0 -251
  58. {auditable-0.1.0 → auditable-0.2.0}/LICENSE +0 -0
  59. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/_report_figures.py +0 -0
  60. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/audit_report.py +0 -0
  61. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/chain.py +0 -0
  62. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/compound.py +0 -0
  63. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/adapters/declared_plan.py +0 -0
  64. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/features.py +0 -0
  65. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/graph/pre.py +0 -0
  66. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/harness/__init__.py +0 -0
  67. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/model/__init__.py +0 -0
  68. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/record.py +0 -0
  69. {auditable-0.1.0 → auditable-0.2.0}/src/auditable/report.py +0 -0
  70. {auditable-0.1.0 → auditable-0.2.0}/tests/test_audit_report.py +0 -0
  71. {auditable-0.1.0 → auditable-0.2.0}/tests/test_auditors.py +0 -0
  72. {auditable-0.1.0 → auditable-0.2.0}/tests/test_basic.py +0 -0
  73. {auditable-0.1.0 → auditable-0.2.0}/tests/test_compound.py +0 -0
  74. {auditable-0.1.0 → auditable-0.2.0}/tests/test_gate.py +0 -0
  75. {auditable-0.1.0 → auditable-0.2.0}/tests/test_graph.py +0 -0
  76. {auditable-0.1.0 → auditable-0.2.0}/tests/test_graph_execution_reach.py +0 -0
  77. {auditable-0.1.0 → auditable-0.2.0}/tests/test_graph_features.py +0 -0
  78. {auditable-0.1.0 → auditable-0.2.0}/tests/test_pre.py +0 -0
  79. {auditable-0.1.0 → auditable-0.2.0}/tests/test_pre_coverage.py +0 -0
  80. {auditable-0.1.0 → auditable-0.2.0}/tests/test_replay.py +0 -0
  81. {auditable-0.1.0 → auditable-0.2.0}/tests/test_report.py +0 -0
  82. {auditable-0.1.0 → auditable-0.2.0}/tests/test_sink.py +0 -0
@@ -24,5 +24,9 @@ env/
24
24
  # MkDocs build output
25
25
  site/
26
26
 
27
+ # Local-only notes and credentials (convention: *.local.md is never committed).
28
+ # Holds internal testing notes and any copied key file (e.g. keys.local.md).
29
+ *.local.md
30
+
27
31
  # experiment data cache
28
32
  experiment/.cache/
@@ -0,0 +1,389 @@
1
+ Metadata-Version: 2.4
2
+ Name: auditable
3
+ Version: 0.2.0
4
+ Summary: An open-source system of record for AI-agent decisions: it captures what each decision relied on, replays it against live state, and rolls back the committed action when it no longer holds.
5
+ Project-URL: Homepage, https://github.com/yzhao062/auditable
6
+ Project-URL: Repository, https://github.com/yzhao062/auditable
7
+ Project-URL: Issues, https://github.com/yzhao062/auditable/issues
8
+ Author-email: Yue Zhao <yzhao010@usc.edu>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: agent,agent-reliability,ai-agents,ai-risk,ai-safety,anomaly-detection,audit,auditing,compliance,langchain,langgraph,llm,mcp,model-context-protocol,observability,replay,trustworthy-ai
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: anomaly
25
+ Requires-Dist: pyod>=1.1.0; extra == 'anomaly'
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.0; extra == 'dev'
28
+ Requires-Dist: pyod>=1.1.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: twine>=5.0; extra == 'dev'
31
+ Provides-Extra: graph
32
+ Requires-Dist: networkx>=3.0; extra == 'graph'
33
+ Provides-Extra: langchain
34
+ Requires-Dist: langchain-core<2,>=0.3.0; extra == 'langchain'
35
+ Provides-Extra: langgraph
36
+ Requires-Dist: langgraph>=1.0; (python_version >= '3.10') and extra == 'langgraph'
37
+ Requires-Dist: networkx>=3.0; extra == 'langgraph'
38
+ Provides-Extra: llm
39
+ Requires-Dist: openai>=1.0; extra == 'llm'
40
+ Provides-Extra: mcp
41
+ Requires-Dist: mcp<2,>=1.28.1; (python_version >= '3.10') and extra == 'mcp'
42
+ Provides-Extra: report
43
+ Requires-Dist: fpdf2>=2.7; extra == 'report'
44
+ Requires-Dist: matplotlib>=3.5; extra == 'report'
45
+ Requires-Dist: networkx>=3.0; extra == 'report'
46
+ Description-Content-Type: text/markdown
47
+
48
+ <div align="center">
49
+
50
+ # auditable
51
+
52
+ **`auditable` is an open-source system of record for AI-agent decisions: it captures what each decision relied on, replays it against live state, and rolls back the committed action when it no longer holds.**
53
+
54
+ *Your logs show what the agent did. `auditable` shows what it relied on, replays it under live state, and rolls it back when it no longer holds.*
55
+
56
+ [![PyPI](https://img.shields.io/pypi/v/auditable.svg)](https://pypi.org/project/auditable/)
57
+ [![Python](https://img.shields.io/pypi/pyversions/auditable.svg)](https://pypi.org/project/auditable/)
58
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
59
+ [![Stars](https://img.shields.io/github/stars/yzhao062/auditable.svg?style=social)](https://github.com/yzhao062/auditable)
60
+
61
+ [Compare](#auditable-vs-the-tools-you-already-use) · [Flagship Demo](#the-flagship-moment) · [Plug In Your Agent](#plug-in-your-agent) · [Lifecycle](#the-lifecycle) · [Install](#install) · [Docs](https://auditable-ai.readthedocs.io/) · [Roadmap](#roadmap)
62
+
63
+ ![One typed two-layer decision graph at the center, read by three lifecycle attach points: PRE lints a declared plan before deploy, LIVE replays and recovers while the agent runs, and POST ranks a finished run](https://raw.githubusercontent.com/yzhao062/auditable/main/assets/lifecycle.png)
64
+
65
+ </div>
66
+
67
+ ### auditable vs. the Tools You Already Use
68
+
69
+ | Capability | **auditable** | Tracing and Observability<br><sub>[LangSmith](#ref-1), [Langfuse](#ref-2), [Phoenix](#ref-3)</sub> | Eval Harnesses<br><sub>[DeepEval](#ref-5), [Ragas](#ref-6), [promptfoo](#ref-7)</sub> | Guardrails<br><sub>[NeMo](#ref-9), [Guardrails AI](#ref-10), [Lakera](#ref-11)</sub> |
70
+ |---|:---:|:---:|:---:|:---:|
71
+ | Captures the dependency state a decision relied on | ✅ | 🟡 | ❌ | ❌ |
72
+ | Re-decides under live state (replay) | ✅ | 🟡 | ❌ | ❌ |
73
+ | Reverses a committed action (rollback) | ✅ | ❌ | ❌ | ❌ |
74
+ | One graph across plan, run, and review | ✅ | 🟡 | ❌ | ❌ |
75
+ | Framework-agnostic, dependency-free core | ✅ | 🟡 | 🟡 | 🟡 |
76
+
77
+ <sub>✅ yes · 🟡 partial · ❌ no, per each tool's public docs (2026). These categories are complementary to `auditable`, not competitors: tracing captures execution, evals score quality, guardrails block at runtime. `auditable` is the audit-and-recovery layer that plugs into them. See [References](#references).</sub>
78
+
79
+ **`auditable` is the only column with the full set.** What makes it new:
80
+
81
+ - **A unified graph representation for agentic AI.** Every agent run becomes one typed graph that PRE, LIVE, and POST all read. One representation from plan to live operation to review, instead of three disconnected tools.
82
+ - **Recover, do not just observe.** `auditable` captures the dependency state a decision relied on, replays it under the state that is live now, and reverses the committed action through a compensating rail when it no longer holds. Logging tells you what broke; `auditable` undoes it.
83
+ - **One decision, three spans, judged together.** Data, model, and harness are bound in a single signed, hash-chained record, so a decision is audited as one unit, not three disconnected logs.
84
+
85
+ ### Proven on Public Agent Benchmarks
86
+
87
+ The graph is not just structure. Across six public agent corpora (GRADE, [arXiv:2606.22741](https://arxiv.org/abs/2606.22741)), the dependency layer predicts which runs fail at **ROC-AUC 0.805** where run length carries no signal, and the execution layer localizes the faulting step at **Top-3 0.614**. See [the benchmark detail](docs/post-analysis.md) for the full numbers and corpora.
88
+
89
+ <p align="center">
90
+ <img src="https://raw.githubusercontent.com/yzhao062/auditable/main/assets/grade-transfer.png" alt="Leave-one-corpus-out transfer ROC-AUC: the size-normalized dependency signal clears chance on all six held-out agent corpora, while run size drops below chance on tau-bench and SWE-Gym" width="49%">
91
+ <img src="https://raw.githubusercontent.com/yzhao062/auditable/main/assets/grade-localization.png" alt="Step-level fault localization on Who and When: ranking steps by execution-graph structure beats an early-fault position prior on top-1, top-3, and MRR" width="49%">
92
+ </p>
93
+ <p align="center"><sub>Figures from GRADE (<a href="https://arxiv.org/abs/2606.22741">arXiv:2606.22741</a>).</sub></p>
94
+
95
+ Agents act on dependency state that quietly drifts. A budget read minutes ago can fall below the amount already committed; a price pinned at plan time can move before the action lands. Most tools log what happened, yet they cannot re-decide under the state that is live now, so a stale decision stands until a human notices. `auditable` closes that recovery gap across the lifecycle on one graph: it captures the decision, replays it against live state, and reverses the committed action when it no longer holds.
96
+
97
+ ## The Flagship Moment
98
+
99
+ One payment, walked through the whole lifecycle in 18 `auditable` calls. The agent approves a $2,083.20 vendor payment against a budget snapshot that covered it. Six days later the live budget has dropped below the amount. `replay` re-decides on the live state, and the gate reverses the committed payment. This is recovery, not a log line.
100
+
101
+ ```bash
102
+ pip install "auditable[graph]"
103
+ python examples/example_end_to_end.py
104
+ ```
105
+
106
+ The run prints a single audit report: a REVIEW verdict, the keystone with its coverage reason, six findings with severity tags and recommended actions, and the LIVE recovery that rolled the payment back. Paste it into a pull request or an issue.
107
+
108
+ ![Audit report page for a payment-approver run. A REVIEW verdict banner sits above a roll-up of zero blocks, one rollback, and five PRE lints. Sections below list the keystone decision with its coverage reason, six numbered findings each carrying a severity tag and a recommended action, and a LIVE recovery tally showing the rolled-back payment record.](https://raw.githubusercontent.com/yzhao062/auditable/main/assets/audit-report-sample.png)
109
+
110
+ ## Plug In Your Agent
111
+
112
+ `auditable` works with the agent you already run. Wrap a LangGraph `StateGraph` of plain sync or async function nodes over TypedDict state, and every node's reads and writes over the state channels become **observed** dependency edges, matched across the superstep barrier, with no change to your node logic:
113
+
114
+ ```python
115
+ from langgraph.graph import StateGraph
116
+ from auditable import analyze_run
117
+ from auditable.integrations.langgraph import instrument
118
+
119
+ builder = instrument(StateGraph(State)) # 1) wrap once, then build / compile / invoke as usual
120
+ ...
121
+ graph = builder.compile()
122
+ graph.invoke(initial_state)
123
+ report = analyze_run(builder, adapter=builder) # 2) the observed dependency graph for that run
124
+ print(report.keystone) # the step the rest of the run rests on
125
+ ```
126
+
127
+ ```bash
128
+ pip install "auditable[langgraph]"
129
+ python examples/example_langgraph_capture.py # a real LangGraph run -> observed=100%, keystone named
130
+ ```
131
+
132
+ The same wrapped builder also yields **replayable** records, so the LIVE pillar runs on the captured run, not a hand-built dict. `builder.to_records(decisions={"approve": "vendor_payment"})` lowers a marked decision node into a `DecisionRecord` carrying the state it relied on, and `replay(record, live_state=...)` re-decides it under state that is live now and routes a rollback. See [`example_langgraph_live_replay.py`](examples/example_langgraph_live_replay.py): one real LangGraph run, captured, ranked (POST), then replayed and reversed under a drifted budget (LIVE).
133
+
134
+ Want a real model in the loop? [`example_langgraph_llm_agent.py`](examples/example_langgraph_llm_agent.py) runs the same capture with live LLM nodes against any OpenAI-compatible endpoint (a plain OpenAI key, a gateway, or a local vLLM or Ollama server). The captured edges are identical, because the capture sees the state channels a node read and wrote, not the model call inside it.
135
+
136
+ Not on LangGraph? A one-line callback captures any LangChain runnable (`auditable.integrations.langchain`), an in-place `instrument` records every tool call at an MCP server boundary (`auditable.integrations.mcp`), and the framework-agnostic `TouchRecorder` captures the same observed edges from any loop (a raw OpenAI or Anthropic agent, your own scheduler) by declaring each step's `reads()` and `writes()`. See [`examples/example_touch_capture.py`](examples/example_touch_capture.py). **Roadmap:** CrewAI and OpenTelemetry.
137
+
138
+ ## Examples and Integrations
139
+
140
+ Point `auditable` at a scenario and it builds the same typed graph. Each row links a runnable example; browse them all in [`examples/README.md`](examples/README.md).
141
+
142
+ | Scenario | Pillar | Builds the graph from | Run |
143
+ |---|---|---|---|
144
+ | Capture a real LangGraph agent | LIVE → POST | a live `StateGraph` run (`instrument`) | [`example_langgraph_capture.py`](examples/example_langgraph_capture.py) |
145
+ | Capture a real LLM agent (live model) | LIVE → POST | a model-driven `StateGraph` run (`instrument` + OpenAI-compatible) | [`example_langgraph_llm_agent.py`](examples/example_langgraph_llm_agent.py) |
146
+ | Capture a real LangChain runnable | LIVE → POST | a callback on any runnable (`LangChainCallbackHandler`) | [`example_langchain_capture.py`](examples/example_langchain_capture.py) |
147
+ | Capture any tool loop by hand | LIVE → POST | declared resource touches (`TouchRecorder`) | [`example_touch_capture.py`](examples/example_touch_capture.py) |
148
+ | Capture MCP tool traffic at the server boundary | LIVE → POST | an instrumented FastMCP server (`instrument`) | [`example_mcp_capture.py`](examples/example_mcp_capture.py) |
149
+ | Capture a LangGraph agent, then replay and reverse it | LIVE → POST | a live `StateGraph` run lowered to replayable records (`instrument` + `to_records`) | [`example_langgraph_live_replay.py`](examples/example_langgraph_live_replay.py) |
150
+ | Lint a declared plan before deploy | PRE | a framework-agnostic plan dict (`declared_plan_v1`) | [`example_pre_lint_plan.py`](examples/example_pre_lint_plan.py) |
151
+ | Recover a payment as the budget drifts | LIVE | a decision captured live (`audit` + `replay`) | [`example_live_replay.py`](examples/example_live_replay.py) |
152
+ | Monitor a run as it streams, name the keystone live | LIVE | a run scored prefix by prefix (`LiveSession`) | [`example_live_monitor.py`](examples/example_live_monitor.py) |
153
+ | Rank a tau-bench run, name the keystone | POST | a tau-bench trajectory (`tau_bench_prior_db_reads_v1`) | [`example_post_rank_run.py`](examples/example_post_rank_run.py) |
154
+ | Walk one payment through every pillar | PRE, LIVE, POST | the full lifecycle (`own_record_v1` for POST) | [`example_end_to_end.py`](examples/example_end_to_end.py) |
155
+
156
+ The first six rows capture a real run; see [Plug In Your Agent](#plug-in-your-agent) for the two-line setup. **Roadmap:** CrewAI and OpenTelemetry.
157
+
158
+ ## The Lifecycle
159
+
160
+ `auditable` runs the same detection-and-report pass over one typed decision graph at three points in an agent's life. The graph kernel stays constant; only the pillar changes (when it fires, what it scores).
161
+
162
+ | Pillar | When It Fires | Public Entry | Focus |
163
+ |---|---|---|---|
164
+ | **PRE** | Before deploy | `analyze_plan` | Read-only structural lints on a declared plan. Names the control-flow chokepoint. Dependency-state risk withheld. |
165
+ | **LIVE** | While running | `audit` + `replay` + `ActionGate` | Capture a decision, re-decide under live state, route a fix (allow, block, review, rollback) through a rail. The sharpest pillar. |
166
+ | **POST** | After a run | `analyze_run` | Rank a finished run by structural blast share. Name the keystone the run rests on, so you review that step first. |
167
+
168
+ ## Install
169
+
170
+ ```bash
171
+ pip install auditable
172
+ ```
173
+
174
+ The core is dependency-free and torch-free. Structural-graph analysis (`analyze_plan` for PRE, `analyze_run` for POST) needs the optional graph extra (NetworkX):
175
+
176
+ ```bash
177
+ pip install "auditable[graph]"
178
+ pip install "auditable[anomaly]"
179
+ ```
180
+
181
+ The graph extra adds NetworkX. The anomaly extra adds the fitted `DataAuditor` path
182
+ through PyOD; freshness fallback and the LIVE example run on the core install alone.
183
+
184
+ Docs: https://auditable-ai.readthedocs.io/
185
+
186
+ ## The Three Pillars, in Detail
187
+
188
+ <details>
189
+ <summary><b>PRE: lint the plan before deploy</b> (four read-only lints, the chokepoint, a coverage report)</summary>
190
+
191
+ Point `analyze_plan` at a declared plan (a plain dict, the neutral target a LangGraph, CrewAI, or AutoGen front-end would lower into) and it runs read-only structural lints over the plan graph. Every check is a pure NetworkX query: no value is executed, and every finding is a structural design warning, not a validated failure prediction.
192
+
193
+ ```python
194
+ from auditable.graph.pre import analyze_plan
195
+ from auditable.graph.adapters import declared_plan_v1
196
+
197
+ plan = {
198
+ "nodes": [
199
+ # 0: read a volatile price, but grant scope far beyond what it read.
200
+ {"idx": 0, "agent": "planner", "kind": "tool_call",
201
+ "reads": [{"id": "price", "volatile": True}],
202
+ "scope": ["price", "ledger", "vendor_db"]},
203
+ # 1: a decision that rests on the unpinned, un-revalidated price.
204
+ {"idx": 1, "agent": "planner", "kind": "decision",
205
+ "reads": [{"id": "price", "producer": 0, "volatile": True}],
206
+ "control_preds": [0]},
207
+ # 2: a consequential write of 'order', with no prior read of 'order'
208
+ # and no re-read of 'price' between the volatile read and the action.
209
+ {"idx": 2, "agent": "executor", "kind": "tool_call",
210
+ "reads": [{"id": "price", "producer": 0, "volatile": True}],
211
+ "writes": ["order"], "control_preds": [1]},
212
+ ]
213
+ }
214
+
215
+ report = analyze_plan(plan, adapter=declared_plan_v1)
216
+ print(report)
217
+ ```
218
+
219
+ The four shipping lints, all read-only queries at `severity='warning'`:
220
+
221
+ | Lint | Fires When |
222
+ |---|---|
223
+ | `write_with_no_prior_read` | A node writes a resource that nothing in its backward slice ever read. |
224
+ | `flippable_dependency_annotation` | An unpinned, non-revalidated volatile dependency feeds a decision. This is an annotation; the would-it-flip question needs runtime values and is out of scope at PRE. |
225
+ | `scope_vs_snapshot` | Granted tool scope strictly exceeds the snapshot the node read. |
226
+ | `missing_revalidation_barrier` | A volatile read reaches a consequential action with no intervening re-read. Drift confirmation needs runtime values and is out of scope at PRE. |
227
+
228
+ The report also names the **execution-topology keystone**: the structural chokepoint of the declared plan, the node that the most other nodes transitively follow in control flow (the argmax of `execution_reach` over the `handoff_to` projection). This is a structural design lint, a separate concept from the POST blast-radius keystone, and it does not predict failure.
229
+
230
+ Alongside the lints, the **Preflight Coverage Report** is a descriptive coverage-readiness view, explicitly not a risk score. It reports the dependency-edge grade mix, the observed fraction, the saturation ratio, the exact no-score reason the runtime scorer would apply, which declared reads, writes, and edges still lack a resource identity, and the declared revalidation barriers per resource.
231
+
232
+ Two boundaries, stated plainly. Dependency-state blast-share risk is **withheld** at PRE: the declared dependency layer is declared-only (observed fraction zero), so `analyze_plan` returns `state_b_risk=None` with `state_b_withheld=True` and a reason string, and it raises rather than emit a number if a scored verdict ever came back. A table-stakes OWASP-Agentic and CWE rule floor is **planned**, not shipping.
233
+
234
+ See [`examples/example_pre_lint_plan.py`](examples/example_pre_lint_plan.py) and the [PRE rules reference](docs/pre-rules.md).
235
+
236
+ </details>
237
+
238
+ <details>
239
+ <summary><b>POST: rank a finished run, find the keystone</b> (`analyze_run` over a recorded trajectory)</summary>
240
+
241
+ `analyze_run` reads a recorded agent run, builds one decision graph, and ranks every step by how much of the run transitively rests on it, so you review the keystone first. On a tau-bench airline trajectory, the one reservation read that both later writes depend on is the keystone.
242
+
243
+ ![auditable analyze_run ranks a recorded tau-bench run by structural blast share and names the keystone decision](https://raw.githubusercontent.com/yzhao062/auditable/main/assets/analyze_run.png)
244
+
245
+ ```python
246
+ from auditable import analyze_run
247
+ from auditable.graph.adapters import tau_bench_prior_db_reads_v1
248
+
249
+ report = analyze_run(run, adapter=tau_bench_prior_db_reads_v1)
250
+ k = report.keystone
251
+ print(k.idx, k.node_attrs["tool"]) # 2 get_reservation_details
252
+ ```
253
+
254
+ The score is an uncalibrated triage ranking, not a calibrated probability. In a no-score state (`no_score:single_decision`, `no_score:low_coverage`) the scores are `None`, so a withheld score never reads as zero risk. The corpus write-to-read edges are modeled (a conservative prior-read upper bound, not a causal label); the report carries these caveats in `report.notes`. The trajectory is modeled on [tau-bench](https://github.com/sierra-research/tau-bench) (Sierra Research, MIT). See [`examples/example_post_rank_run.py`](examples/example_post_rank_run.py) and the [POST analysis reference](docs/post-analysis.md).
255
+
256
+ </details>
257
+
258
+ <details>
259
+ <summary><b>How It Works: the two-layer graph, the signed record, the rail</b> (kernel internals)</summary>
260
+
261
+ ![auditable models one decision as a two-layer graph: an execution layer (control flow, observed from the trace) over a dependency layer (what each step relied on); replay catches a step that rested on a value that has since gone stale](https://raw.githubusercontent.com/yzhao062/auditable/main/assets/twolayer.png)
262
+
263
+ *auditable links a run into one graph with two edge layers: execution (control flow, observed from the trace) over dependency (what each step relied on). When a step rested on a value that has since gone stale, like `price`, `replay` catches it. The record itself binds three spans per decision (data, model, harness).*
264
+
265
+ The graph kernel has two edge layers, and the distinction is load-bearing. Execution edges (`emits`, `handoff_to`) are observed from the trace. Dependency edges (`depends_on`) are declared, inferred, or observed, and every edge records how it is known: the corpus and plan adapters declare or infer them, while the LangGraph capture path and the `TouchRecorder` read them off a real run as observed channel touches. PRE and POST both run over this same typed graph; `audit()` is the ergonomic capture entry, so you are never asked to build the graph by hand.
266
+
267
+ One agent decision crosses three spans, and `auditable` binds all three in a single signed, hash-chained record:
268
+
269
+ | Span | What the Record Binds | Signal in v0.1 |
270
+ |---|---|---|
271
+ | **Data** | What the agent read and the dependency snapshot it relied on | Snapshot freshness |
272
+ | **Model** | Which model produced the output, and its stated basis | Decision-basis trust flag |
273
+ | **Harness** | The action executed and its cost | A static cost-cap rule, plus the replay verdict |
274
+
275
+ `replay()` re-derives whether the action still holds under the live dependency state versus the snapshot the agent used, and returns one of four routed verdicts: `ALLOW`, `ROLLBACK` (justified on the snapshot but not on live state, the stale-state case), `BLOCK` (justified on neither), or `HUMAN_REVIEW` (the policy raised `ReplayUndecidable`). A `Policy` is any callable `(state, action) -> (justified, reason)`. The `ActionGate` then executes that verdict through a `Rail`: a post-commit `ROLLBACK` or `BLOCK` calls `rail.compensate(receipt)` to reverse a committed action, rather than printing a recommendation. The shipped `ReferenceLedger` is an in-process reference rail for demos and tests; it is not a production payment rail.
276
+
277
+ Records are signed and hash-chained: each record carries a `prev_digest` and a content-addressed `record_id`. Two sinks ship today, `MemorySink` (in-process) and `FileSink` (append-only JSONL, durable across process exit, fails closed on a corrupt tail).
278
+
279
+ Ingestion is source-agnostic through the public `Adapter` protocol. The corpus and plan adapters ship `tau_bench_prior_db_reads_v1` (public-corpus trajectory, POST), `own_record_v1` (auditable's own signed records, POST), and `declared_plan_v1` (a framework-agnostic declared plan dict, PRE). For a real run, the LangGraph capture path (`auditable.integrations.langgraph.instrument`) and the generic `TouchRecorder` produce observed dependency edges with a structured `ResourceRef`, matched superstep-aware and reducer-aware. The declared-plan adapter stays the neutral PRE seam a LangGraph or CrewAI front-end would lower a plan into; it is not a parser for any framework.
280
+
281
+ See the [architecture reference](docs/architecture.md) for the full kernel, adapters, and sinks.
282
+
283
+ </details>
284
+
285
+ <details>
286
+ <summary><b>Using a single layer</b> (standalone auditors as inputs to the record)</summary>
287
+
288
+ Each span's check is a standalone `Auditor` that runs on its own, with no agent and no chain, and returns a signed `Report`. `DataAuditor` scores snapshot freshness, `ModelAuditor` produces a decision-basis trust flag, and `HarnessAuditor` applies one static cost-cap rule.
289
+
290
+ ```python
291
+ import time
292
+ from auditable import DataAuditor, DependencySnapshot
293
+
294
+ snapshot = DependencySnapshot(state={"budget_remaining": 1000}, captured_at=time.time() - 7 * 86400)
295
+ report = DataAuditor(max_age_seconds=86400).assess(snapshot)
296
+ print(report.flag, report.score) # stale 1.0
297
+ ```
298
+
299
+ These standalone auditors are inputs to the record, not the headline. The composition (capture, replay, recovery, and the lifecycle pillars) is the main line; the modules feed it. See [`examples/example_standalone_report.py`](examples/example_standalone_report.py).
300
+
301
+ </details>
302
+
303
+ <details>
304
+ <summary><b>Scope, stated honestly</b> (what ships today vs. what is planned)</summary>
305
+
306
+ **What ships today.** The full signed chain, replay under live state, executed recovery through a rail-neutral gate, two sinks (in-memory and append-only JSONL), the POST `analyze_run` ranking, the PRE `analyze_plan` lints plus preflight coverage report, and real-run capture into observed dependency edges (LangGraph via `instrument`, any loop via `TouchRecorder`).
307
+
308
+ The fitted data-anomaly path ships as an unpromoted module API, and `LiveSession`
309
+ prefix scoring ships. The release does **not** yet claim benchmark-grade detector
310
+ evidence, a calibrated model-trust score, or calibrated cross-layer risk. The POST
311
+ structural score is an uncalibrated ranking; the compound report is a transparent,
312
+ explicitly uncalibrated debug bundle. Calibrated compound scoring remains on the
313
+ [roadmap](#roadmap).
314
+
315
+ </details>
316
+
317
+ ## Roadmap
318
+
319
+ <details>
320
+ <summary>What ships today, and what is next</summary>
321
+
322
+ <br>
323
+
324
+ Shipping today: the full capture, replay, and recover chain; PRE plan lints; POST run ranking; `LiveSession` prefix scoring; signed, hash-chained records; two sinks. The fitted PyOD detector over dependency snapshots also ships as an unpromoted module API, with freshness as the fallback; promotion waits for benchmark-grade evidence. Next:
325
+
326
+ - **One calibrated compound score.** Combine the data, model, and action signals into a single calibrated score, with the model treated as a first-class node in the graph.
327
+ - **Automatic fixes.** Refresh stale data, quarantine bad inputs, fall back to a safer model, or require human sign-off.
328
+ - **Built-in CI checks.** A table-stakes OWASP-Agentic and CWE rule floor for continuous integration.
329
+ - **Integrations (1.0).** LangGraph capture (`instrument`), the LangChain callback, and MCP tool-boundary capture ship now; next are a CrewAI hook, export to OpenTelemetry and LangSmith, downloadable evidence bundles, and a stable public API.
330
+
331
+ </details>
332
+
333
+ ## Related Projects
334
+
335
+ `auditable` is part of a small family of open agent-reliability projects:
336
+
337
+ - **[awesome-auditable-ai](https://github.com/yzhao062/awesome-auditable-ai)** — a curated reading list on AI agent reliability and auditing.
338
+ - **[GRADE](https://arxiv.org/abs/2606.22741)** — the research behind the typed two-layer graph this library is built on (also in [References](#references)).
339
+
340
+ ## References
341
+
342
+ <details>
343
+ <summary>Compared tools, benchmarks, and method</summary>
344
+
345
+ <br>
346
+
347
+ The capability marks in the table above are per each project's public documentation as of 2026.
348
+
349
+ **Compared tools**
350
+
351
+ <a id="ref-1"></a>`[1]` [LangSmith](https://docs.langchain.com/langsmith)<br>
352
+ <a id="ref-2"></a>`[2]` [Langfuse](https://github.com/langfuse/langfuse)<br>
353
+ <a id="ref-3"></a>`[3]` [Arize Phoenix](https://github.com/Arize-ai/phoenix)<br>
354
+ <a id="ref-4"></a>`[4]` [Weights & Biases Weave](https://github.com/wandb/weave)<br>
355
+ <a id="ref-5"></a>`[5]` [DeepEval](https://github.com/confident-ai/deepeval)<br>
356
+ <a id="ref-6"></a>`[6]` [Ragas](https://github.com/explodinggradients/ragas)<br>
357
+ <a id="ref-7"></a>`[7]` [promptfoo](https://github.com/promptfoo/promptfoo)<br>
358
+ <a id="ref-8"></a>`[8]` [OpenAI Evals](https://github.com/openai/evals)<br>
359
+ <a id="ref-9"></a>`[9]` [NeMo Guardrails](https://github.com/NVIDIA-NeMo/Guardrails)<br>
360
+ <a id="ref-10"></a>`[10]` [Guardrails AI](https://github.com/guardrails-ai/guardrails)<br>
361
+ <a id="ref-11"></a>`[11]` [Lakera Guard](https://www.lakera.ai/lakera-guard)
362
+
363
+ **Benchmarks and datasets**
364
+
365
+ <a id="ref-12"></a>`[12]` [tau-bench](https://github.com/sierra-research/tau-bench) (arXiv:2406.12045)<br>
366
+ <a id="ref-13"></a>`[13]` [Who&When: Which Agent Causes Task Failures and When?](https://github.com/ag2ai/Agents_Failure_Attribution) (arXiv:2505.00212)
367
+
368
+ **Method**
369
+
370
+ <a id="ref-14"></a>`[14]` [GRADE: Graph Representation of LLM Agent Dependency and Execution](https://arxiv.org/abs/2606.22741)
371
+
372
+ </details>
373
+
374
+ ## Citation
375
+
376
+ If you use `auditable` in your work, please cite the GRADE paper, the typed two-layer graph model the library is built on:
377
+
378
+ ```bibtex
379
+ @article{zhao2026grade,
380
+ title = {GRADE: Graph Representation of LLM Agent Dependency and Execution},
381
+ author = {Zhao, Yue},
382
+ journal = {arXiv preprint arXiv:2606.22741},
383
+ year = {2026}
384
+ }
385
+ ```
386
+
387
+ ## License
388
+
389
+ [Apache-2.0](LICENSE).