faultgate 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 (39) hide show
  1. faultgate-0.1.0/CHANGELOG.md +14 -0
  2. faultgate-0.1.0/LICENSE +21 -0
  3. faultgate-0.1.0/MANIFEST.in +3 -0
  4. faultgate-0.1.0/PKG-INFO +225 -0
  5. faultgate-0.1.0/README.md +199 -0
  6. faultgate-0.1.0/examples/gate/band.json +77 -0
  7. faultgate-0.1.0/examples/gate/baseline_glm_run1.json +19982 -0
  8. faultgate-0.1.0/examples/gate/baseline_glm_run2.json +18956 -0
  9. faultgate-0.1.0/examples/gate/candidate_gpt.json +26290 -0
  10. faultgate-0.1.0/examples/gate/candidate_qwen.json +18538 -0
  11. faultgate-0.1.0/examples/gate/golden.json +32 -0
  12. faultgate-0.1.0/examples/gate/policy.json +1 -0
  13. faultgate-0.1.0/examples/golden.json +7 -0
  14. faultgate-0.1.0/examples/traces.json +1539 -0
  15. faultgate-0.1.0/pyproject.toml +41 -0
  16. faultgate-0.1.0/setup.cfg +4 -0
  17. faultgate-0.1.0/src/faultgate/__init__.py +2 -0
  18. faultgate-0.1.0/src/faultgate/band.py +78 -0
  19. faultgate-0.1.0/src/faultgate/checks.py +238 -0
  20. faultgate-0.1.0/src/faultgate/cli.py +173 -0
  21. faultgate-0.1.0/src/faultgate/judge.py +123 -0
  22. faultgate-0.1.0/src/faultgate/runner.py +88 -0
  23. faultgate-0.1.0/src/faultgate/trace.py +193 -0
  24. faultgate-0.1.0/src/faultgate.egg-info/PKG-INFO +225 -0
  25. faultgate-0.1.0/src/faultgate.egg-info/SOURCES.txt +37 -0
  26. faultgate-0.1.0/src/faultgate.egg-info/dependency_links.txt +1 -0
  27. faultgate-0.1.0/src/faultgate.egg-info/entry_points.txt +2 -0
  28. faultgate-0.1.0/src/faultgate.egg-info/requires.txt +10 -0
  29. faultgate-0.1.0/src/faultgate.egg-info/top_level.txt +1 -0
  30. faultgate-0.1.0/tests/__init__.py +0 -0
  31. faultgate-0.1.0/tests/conftest.py +32 -0
  32. faultgate-0.1.0/tests/test_band.py +79 -0
  33. faultgate-0.1.0/tests/test_checks.py +35 -0
  34. faultgate-0.1.0/tests/test_cli.py +46 -0
  35. faultgate-0.1.0/tests/test_golden.py +48 -0
  36. faultgate-0.1.0/tests/test_judge.py +43 -0
  37. faultgate-0.1.0/tests/test_policy.py +68 -0
  38. faultgate-0.1.0/tests/test_runner.py +22 -0
  39. faultgate-0.1.0/tests/test_trace.py +69 -0
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-09-22
4
+
5
+ - `faultgate check <traces.json>`: run checks over an OTLP/JSON GenAI-semconv trace export; exit 1 if any fires.
6
+ - Rule checks (no model): `search_loop` (κ 0.98 on 200 human-labelled runs, 1.00 on the P5 split), `abstention` (1/1, 0 FP).
7
+ - `policy` rule + `--policy policy.json` (allowlist, denied argument patterns, max argument size, call budget) — FAULTLINE P16 after the fact; P9's four structural attack shapes covered by tests; 0 FP on 290 real traces.
8
+ - Golden checks + `--golden golden.json` (keyed by scenario or prompt): `wrong_answer` (4/4 vs human verdict, 0 FP), `truncation` (1/1, 0 FP).
9
+ - Composite GitHub Action (`uses: samirsawarkar/faultgate@<ref>`), self-tested in CI against the shipped examples.
10
+ - `laya` is an extra (`faultgate[laya]`); the base install is pydantic only.
11
+ - Judge checks (opt-in `--judge`): `wrong_direction`. Judges: `laya` (local, pinned `1c5edc17`, budgeted render), `litellm:<model>` (`[api]` extra).
12
+ - `faultgate baseline` + `check --band`: tolerance band from ≥ 2 baseline runs (FAULTLINE P11 rule), PASS/WARN/FAIL, named regressions.
13
+ - `scripts/calibrate.py`: score any check/judge against FAULTLINE P4 human labels.
14
+ - Examples: five real runs (`examples/traces.json`); two `glm-5.3-flash` baselines and `gpt-5.6-luna` / `qwen3.7-flash` candidates on P11's 30 golden scenarios (`examples/gate/`).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samir Sawarkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include CHANGELOG.md
2
+ recursive-include examples *.json
3
+ recursive-include tests *.py
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.4
2
+ Name: faultgate
3
+ Version: 0.1.0
4
+ Summary: Local-first release gate for AI agents: check recorded traces for known failure modes through a pluggable judge.
5
+ Author: Samir Sawarkar
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/samirsawarkar/faultgate
8
+ Project-URL: Evidence, https://github.com/samirsawarkar/faultline-ai-reliability
9
+ Keywords: ai,agents,evaluation,reliability,opentelemetry,llm
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Testing
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: pydantic>=2
19
+ Provides-Extra: laya
20
+ Requires-Dist: laya>=0.3.5; extra == "laya"
21
+ Provides-Extra: api
22
+ Requires-Dist: litellm>=1.70; extra == "api"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # faultgate
28
+
29
+ **A local-first release gate for AI agents.** Hand it the traces your agent recorded, it checks every run for known failure modes, measures your run-to-run noise, and exits non-zero only when a candidate is a real regression. Free, offline, no model required for the default checks.
30
+
31
+ ```
32
+ $ faultgate check examples/traces.json
33
+ trace search_loop abstention
34
+ ------------------------------------------------
35
+ fc2cdab304bd FAIL 1.00 ok 1.00
36
+ 6d1f9d67a2d4 ok 1.00 FAIL 1.00
37
+ 8bf3879339eb ok 1.00 ok 1.00
38
+ 3a9e507a0f6d ok 1.00 ok 1.00
39
+ 2ab3b6c1e891 ok 1.00 ok 1.00
40
+
41
+ 2/5 traces failed · search_loop=1, abstention=1 · judge none · 0.0s
42
+ $ echo $?
43
+ 1
44
+ ```
45
+
46
+ Real output, unedited, on five real agent runs (`openai/gpt-5.6-luna` on a multi-hop retrieval task, recorded by [FAULTLINE](https://github.com/samirsawarkar/faultline-ai-reliability)). Trace 1 looped for 12 steps and never answered; trace 2 said it couldn't find the answer; 3–5 answered.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install faultgate
52
+ ```
53
+
54
+ Requires Python 3.11 or newer. One dependency (pydantic). The built-in checks are code — nothing to download. Judges are extras:
55
+
56
+ ```bash
57
+ pip install 'faultgate[laya]' # local judge: downloads Laya once (~800 MB, Apache-2.0, pinned revision), offline after
58
+ pip install 'faultgate[api]' # any API model through litellm
59
+ ```
60
+
61
+ Read [How much to trust a verdict](#how-much-to-trust-a-verdict) before you enable a judge.
62
+
63
+ ## Usage
64
+
65
+ ```bash
66
+ faultgate check traces.json # built-in rule checks
67
+ faultgate check traces.json --json report.json # full per-trace verdicts
68
+ faultgate check traces.json --golden golden.json # expected answers: correctness + truncation
69
+ faultgate check traces.json --policy policy.json # what the agent was allowed to do
70
+ faultgate check traces.json --judge laya # also run judge checks, locally
71
+ faultgate check traces.json --judge litellm:openai/gpt-5.6-luna # any API model
72
+ faultgate checks # list checks and their kind
73
+ faultgate judges # list judge specs
74
+
75
+ faultgate baseline run1.json run2.json --out band.json # measure your noise (>= 2 runs)
76
+ faultgate check candidate.json --band band.json # regression, or just noise?
77
+ ```
78
+
79
+ Exit codes: `0` clean · `1` at least one check fired (or, with `--band`, gate FAIL) · `2` bad input.
80
+
81
+ ### Input format
82
+
83
+ An [OTLP/JSON](https://opentelemetry.io/docs/specs/otlp/#json-protobuf-encoding) export using the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). One trace is one agent run:
84
+
85
+ | Span | Attributes read |
86
+ |---|---|
87
+ | root `invoke_agent` | `gen_ai.agent.name`, `gen_ai.input.messages` (the user prompt), `gen_ai.output.messages` (the final answer), optional `faultgate.termination` (why the run stopped, e.g. `step cap`) |
88
+ | child `execute_tool` | `gen_ai.tool.name`, `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` |
89
+
90
+ Legacy `gen_ai.prompt` / `gen_ai.completion` are accepted as fallback. If your exporter records structure but not content, `faultgate` warns you. [`examples/traces.json`](https://github.com/samirsawarkar/faultgate/blob/main/examples/traces.json) is a complete reference file.
91
+
92
+ ## Checks
93
+
94
+ | Check | Kind | Fires when |
95
+ |---|---|---|
96
+ | `search_loop` | rule | The run ended with no answer at its step budget and most tool calls returned nothing. |
97
+ | `abstention` | rule | The final answer explicitly says the information could not be found. |
98
+ | `policy` | rule | A tool call broke the policy: unlisted tool, denied argument pattern, oversized argument, or call budget. |
99
+ | `wrong_answer` | golden | Answered, and the expected answer is not in the final answer. |
100
+ | `truncation` | golden | The answer has the head of the expected token but not the whole token (`Onyx` for `Onyx-4413`). |
101
+ | `wrong_direction` | judge | On a multi-hop task, the agent walked the dependency chain backwards. |
102
+
103
+ A **rule** is plain code over the trace structure: instant, deterministic, no model. A **golden** check is a rule that also needs the expected answer (`--golden golden.json`, keyed by scenario or by prompt text). A **judge** check is a yes/no question a model answers from the rendered trace; it only runs when you pass `--judge`. All three kinds are calibrated the same way, below.
104
+
105
+ ### Policy: what was the agent allowed to do?
106
+
107
+ ```json
108
+ {"allow": ["search", "lookup", "calc"], "max_calls": 10, "max_arg_chars": 200}
109
+ ```
110
+
111
+ `faultgate check traces.json --policy policy.json` then flags every step that called a tool outside `allow`, passed an argument matching a denied pattern (path traversal, `file://`, `/etc/passwd`, `~/.ssh` out of the box), exceeded `max_arg_chars`, or pushed the run past `max_calls` — with the step number in the reason. It is FAULTLINE P16's runtime policy applied after the fact: the four structural injection categories from P9 (`tool_exfil_path`, `unlisted_tool`, `query_dump`, `budget_loop`) are each caught by a test in `tests/test_policy.py`. Without a policy file only the denied-pattern rule is active, and it fires on 0 of the 290 real traces shipped here.
112
+
113
+ Semantic injections — the agent *obeying* text it read in a document — are a judge question, not a rule, and are not in this release.
114
+
115
+ ## Regression or noise? Tolerance bands
116
+
117
+ Agents are not deterministic. FAULTLINE P11 ran the same model on the same 30 inputs five times and got pass rates from 0.60 to 0.87. A fixed threshold turns that into a coin-flip gate. `faultgate` measures your spread instead:
118
+
119
+ ```
120
+ $ faultgate baseline examples/gate/baseline_glm_run1.json examples/gate/baseline_glm_run2.json --out band.json
121
+ examples/gate/baseline_glm_run1.json: 30 traces · clean rate 0.93 · 0.0s
122
+ examples/gate/baseline_glm_run2.json: 30 traces · clean rate 0.93 · 0.0s
123
+ band [0.93, 0.93] over 30 scenarios × 2 runs · 26 clean in every run · written to band.json
124
+
125
+ $ faultgate check examples/gate/candidate_gpt.json --band band.json
126
+ 18/30 traces failed · search_loop=18, abstention=0 · judge none · 0.0s
127
+ gate: FAIL · clean rate 0.40 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
128
+ regressions (clean in every baseline, not now): r-0208, r-0214, r-0216, r-0224, r-0226, r-0232, r-0258, ...
129
+
130
+ $ faultgate check examples/gate/candidate_qwen.json --band band.json
131
+ gate: FAIL · clean rate 0.70 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
132
+
133
+ $ faultgate check examples/gate/baseline_glm_run2.json --band band.json
134
+ gate: PASS · clean rate 0.93 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
135
+ ```
136
+
137
+ Real runs: two `z-ai/glm-5.3-flash` baselines and `openai/gpt-5.6-luna` / `qwen/qwen3.7-flash` candidates on the same 30 multi-hop scenarios. The verdicts match FAULTLINE P11's live drill (R4 FAIL / R1 FAIL / R2 PASS), which cost $1.84 and an API judge; this took 0.0 s and no model.
138
+
139
+ | Verdict | Rule (n = scenarios in the band) | Exit |
140
+ |---|---|---|
141
+ | **PASS** | clean rate ≥ min observed − 1/n | 0 |
142
+ | **WARN** | in between — a human should look | 0 |
143
+ | **FAIL** | clean rate < min observed − 3/n | 1 |
144
+
145
+ It also lists **regressions**: scenarios clean in *every* baseline run and not clean now — the ones to read first. Scenarios are matched across runs by `faultgate.scenario`, else `gen_ai.agent.id`, else a hash of the prompt.
146
+
147
+ With expected answers the gate grades correctness too, and the numbers land on P11's exactly:
148
+
149
+ ```
150
+ $ faultgate baseline examples/gate/baseline_glm_run*.json --golden examples/gate/golden.json --out band.json
151
+ examples/gate/baseline_glm_run1.json: 30 traces · clean rate 0.80 · 0.0s # P11: 24/30 grounded
152
+ examples/gate/baseline_glm_run2.json: 30 traces · clean rate 0.87 · 0.0s # P11: 26/30 grounded
153
+ band [0.80, 0.87] over 30 scenarios × 2 runs · 21 clean in every run
154
+
155
+ $ faultgate check examples/gate/candidate_gpt.json --golden examples/gate/golden.json --band band.json
156
+ 21/30 traces failed · search_loop=18, abstention=0, policy=0, wrong_answer=3, truncation=3
157
+ gate: FAIL · clean rate 0.30 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70
158
+
159
+ $ faultgate check examples/gate/candidate_qwen.json --golden examples/gate/golden.json --band band.json
160
+ 18/30 traces failed · search_loop=9, abstention=0, policy=0, wrong_answer=9, truncation=8
161
+ gate: FAIL · clean rate 0.40 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70
162
+ ```
163
+
164
+ (Qwen returns the head of the answer token without its suffix in 8 of 30 runs. Nobody had noticed.)
165
+
166
+ ## In CI
167
+
168
+ ```yaml
169
+ - uses: samirsawarkar/faultgate@main
170
+ with:
171
+ traces: traces/candidate.json # exported by your agent's OTel pipeline
172
+ band: traces/band.json # from `faultgate baseline`, committed
173
+ golden: traces/golden.json # optional
174
+ policy: traces/policy.json # optional
175
+ ```
176
+
177
+ Exit 1 on gate FAIL, so the job fails; WARN and PASS pass. The report lands in `faultgate-report.json` for upload as an artifact. The action installs nothing heavier than pydantic; our own CI runs it against this repo's examples and asserts that the GPT candidate fails and the GLM baseline passes.
178
+
179
+ ## How much to trust a verdict
180
+
181
+ This is the part every evaluation tool skips. Every check and judge here is scored against **human failure-mode labels** on 200 real agent runs (FAULTLINE P4), reported on all 200 and on P5's frozen test split (n = 60). Regenerate with `scripts/calibrate.py`.
182
+
183
+ | Check | Kind | Positives | TPR | TNR | κ (n=200) | κ (P5 test, n=60) |
184
+ |---|---|---|---|---|---|---|
185
+ | `search_loop` | rule | 36 | 1.00 | 0.99 | **0.98** | **1.00** |
186
+ | `abstention` | rule | 1 | 1.00 | 1.00 | 1.00 | — (0 positives) |
187
+ | `policy` | rule | synthetic | 4/4 P9 categories | 0 FP on 290 real traces | — | — |
188
+ | `wrong_answer` | golden | 4 | 1.00 | 1.00 | 1.00 | 1.00 |
189
+ | `truncation` | golden | 1 | 1.00 | 1.00 | 1.00 | — (0 positives) |
190
+ | `wrong_direction` | Laya judge | 1 | 1.00 | 0.61 | 0.02 | 0.00 |
191
+
192
+ For comparison, the same `search_loop` question put to judges instead of a rule: Laya κ 0.09, GLM-5.3 (API) κ −0.03. **Where a failure is structural, code beats every model we measured**, which is why the default checks are rules.
193
+
194
+ Read the table honestly:
195
+ - `search_loop` is solid on this task family. The human coder saw the same structural signals, so κ measures agreement with that coding, not ground truth from nowhere.
196
+ - `abstention` and `truncation` have one positive each to learn from. `abstention` is a phrase list; a paraphrase it doesn't know is a miss.
197
+ - `wrong_answer` agrees with the human verdict on all 21 answered runs. It is substring containment, case-insensitive: right for short factual answers, wrong for free-text ones — say what "expected" means for your task before you trust it.
198
+ - `policy` has no human-labelled positives: FAULTLINE's agent rejects hostile calls before they become steps, so its traces never contain an executed attack. Recall is by construction on the P9 attack shapes; the number that matters is zero false positives on real runs.
199
+ - `wrong_direction` is the only check that needs reading, and the only local judge we have is noise on it (fires on 39% of clean runs). It ships because the contract is the product: the day a local model scores κ ≥ 0.7 on it, that's a one-line adapter, and this harness is what proves it.
200
+ - Laya's context is 512 tokens. `faultgate` compacts the trace to fit (answer and termination first, middle steps elided) and counts every call that still overflows; the count is printed.
201
+
202
+ Full confusion matrices and Wilson CIs for the original P5 judges: [FAULTLINE `projects/p05_judge/DECISIONS.md`](https://github.com/samirsawarkar/faultline-ai-reliability/blob/main/projects/p05_judge/DECISIONS.md).
203
+
204
+ ## Swap the judge
205
+
206
+ ```python
207
+ class Judge(Protocol):
208
+ name: str
209
+ budget: int | None # max characters of trace it can read
210
+ def ask(self, state: str, question: str) -> Verdict: ... # Verdict(detected, confidence, reason)
211
+ ```
212
+
213
+ `laya` and `litellm:<model>` ship in the box. Add a judge, run `scripts/calibrate.py --judge yours`, and publish the row.
214
+
215
+ ## What it doesn't do yet
216
+
217
+ - **Premature stop** (answered with an intermediate entity of the chain). Needs the traversal chain, not just the final answer.
218
+ - **Semantic injection checks** (the agent obeyed text it read). P9 has the taxonomy; needs a judge that scores.
219
+ - **Running your agent.** `faultgate` reads traces; it never executes anything.
220
+
221
+ ## Built on FAULTLINE
222
+
223
+ [FAULTLINE](https://github.com/samirsawarkar/faultline-ai-reliability) is the research workbench behind this tool: 30 days of simulator, 16 measured projects, two papers. `faultgate` is the part you install. Every number in this README links back to the run that produced it.
224
+
225
+ MIT © 2026 Samir Sawarkar
@@ -0,0 +1,199 @@
1
+ # faultgate
2
+
3
+ **A local-first release gate for AI agents.** Hand it the traces your agent recorded, it checks every run for known failure modes, measures your run-to-run noise, and exits non-zero only when a candidate is a real regression. Free, offline, no model required for the default checks.
4
+
5
+ ```
6
+ $ faultgate check examples/traces.json
7
+ trace search_loop abstention
8
+ ------------------------------------------------
9
+ fc2cdab304bd FAIL 1.00 ok 1.00
10
+ 6d1f9d67a2d4 ok 1.00 FAIL 1.00
11
+ 8bf3879339eb ok 1.00 ok 1.00
12
+ 3a9e507a0f6d ok 1.00 ok 1.00
13
+ 2ab3b6c1e891 ok 1.00 ok 1.00
14
+
15
+ 2/5 traces failed · search_loop=1, abstention=1 · judge none · 0.0s
16
+ $ echo $?
17
+ 1
18
+ ```
19
+
20
+ Real output, unedited, on five real agent runs (`openai/gpt-5.6-luna` on a multi-hop retrieval task, recorded by [FAULTLINE](https://github.com/samirsawarkar/faultline-ai-reliability)). Trace 1 looped for 12 steps and never answered; trace 2 said it couldn't find the answer; 3–5 answered.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install faultgate
26
+ ```
27
+
28
+ Requires Python 3.11 or newer. One dependency (pydantic). The built-in checks are code — nothing to download. Judges are extras:
29
+
30
+ ```bash
31
+ pip install 'faultgate[laya]' # local judge: downloads Laya once (~800 MB, Apache-2.0, pinned revision), offline after
32
+ pip install 'faultgate[api]' # any API model through litellm
33
+ ```
34
+
35
+ Read [How much to trust a verdict](#how-much-to-trust-a-verdict) before you enable a judge.
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ faultgate check traces.json # built-in rule checks
41
+ faultgate check traces.json --json report.json # full per-trace verdicts
42
+ faultgate check traces.json --golden golden.json # expected answers: correctness + truncation
43
+ faultgate check traces.json --policy policy.json # what the agent was allowed to do
44
+ faultgate check traces.json --judge laya # also run judge checks, locally
45
+ faultgate check traces.json --judge litellm:openai/gpt-5.6-luna # any API model
46
+ faultgate checks # list checks and their kind
47
+ faultgate judges # list judge specs
48
+
49
+ faultgate baseline run1.json run2.json --out band.json # measure your noise (>= 2 runs)
50
+ faultgate check candidate.json --band band.json # regression, or just noise?
51
+ ```
52
+
53
+ Exit codes: `0` clean · `1` at least one check fired (or, with `--band`, gate FAIL) · `2` bad input.
54
+
55
+ ### Input format
56
+
57
+ An [OTLP/JSON](https://opentelemetry.io/docs/specs/otlp/#json-protobuf-encoding) export using the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). One trace is one agent run:
58
+
59
+ | Span | Attributes read |
60
+ |---|---|
61
+ | root `invoke_agent` | `gen_ai.agent.name`, `gen_ai.input.messages` (the user prompt), `gen_ai.output.messages` (the final answer), optional `faultgate.termination` (why the run stopped, e.g. `step cap`) |
62
+ | child `execute_tool` | `gen_ai.tool.name`, `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` |
63
+
64
+ Legacy `gen_ai.prompt` / `gen_ai.completion` are accepted as fallback. If your exporter records structure but not content, `faultgate` warns you. [`examples/traces.json`](https://github.com/samirsawarkar/faultgate/blob/main/examples/traces.json) is a complete reference file.
65
+
66
+ ## Checks
67
+
68
+ | Check | Kind | Fires when |
69
+ |---|---|---|
70
+ | `search_loop` | rule | The run ended with no answer at its step budget and most tool calls returned nothing. |
71
+ | `abstention` | rule | The final answer explicitly says the information could not be found. |
72
+ | `policy` | rule | A tool call broke the policy: unlisted tool, denied argument pattern, oversized argument, or call budget. |
73
+ | `wrong_answer` | golden | Answered, and the expected answer is not in the final answer. |
74
+ | `truncation` | golden | The answer has the head of the expected token but not the whole token (`Onyx` for `Onyx-4413`). |
75
+ | `wrong_direction` | judge | On a multi-hop task, the agent walked the dependency chain backwards. |
76
+
77
+ A **rule** is plain code over the trace structure: instant, deterministic, no model. A **golden** check is a rule that also needs the expected answer (`--golden golden.json`, keyed by scenario or by prompt text). A **judge** check is a yes/no question a model answers from the rendered trace; it only runs when you pass `--judge`. All three kinds are calibrated the same way, below.
78
+
79
+ ### Policy: what was the agent allowed to do?
80
+
81
+ ```json
82
+ {"allow": ["search", "lookup", "calc"], "max_calls": 10, "max_arg_chars": 200}
83
+ ```
84
+
85
+ `faultgate check traces.json --policy policy.json` then flags every step that called a tool outside `allow`, passed an argument matching a denied pattern (path traversal, `file://`, `/etc/passwd`, `~/.ssh` out of the box), exceeded `max_arg_chars`, or pushed the run past `max_calls` — with the step number in the reason. It is FAULTLINE P16's runtime policy applied after the fact: the four structural injection categories from P9 (`tool_exfil_path`, `unlisted_tool`, `query_dump`, `budget_loop`) are each caught by a test in `tests/test_policy.py`. Without a policy file only the denied-pattern rule is active, and it fires on 0 of the 290 real traces shipped here.
86
+
87
+ Semantic injections — the agent *obeying* text it read in a document — are a judge question, not a rule, and are not in this release.
88
+
89
+ ## Regression or noise? Tolerance bands
90
+
91
+ Agents are not deterministic. FAULTLINE P11 ran the same model on the same 30 inputs five times and got pass rates from 0.60 to 0.87. A fixed threshold turns that into a coin-flip gate. `faultgate` measures your spread instead:
92
+
93
+ ```
94
+ $ faultgate baseline examples/gate/baseline_glm_run1.json examples/gate/baseline_glm_run2.json --out band.json
95
+ examples/gate/baseline_glm_run1.json: 30 traces · clean rate 0.93 · 0.0s
96
+ examples/gate/baseline_glm_run2.json: 30 traces · clean rate 0.93 · 0.0s
97
+ band [0.93, 0.93] over 30 scenarios × 2 runs · 26 clean in every run · written to band.json
98
+
99
+ $ faultgate check examples/gate/candidate_gpt.json --band band.json
100
+ 18/30 traces failed · search_loop=18, abstention=0 · judge none · 0.0s
101
+ gate: FAIL · clean rate 0.40 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
102
+ regressions (clean in every baseline, not now): r-0208, r-0214, r-0216, r-0224, r-0226, r-0232, r-0258, ...
103
+
104
+ $ faultgate check examples/gate/candidate_qwen.json --band band.json
105
+ gate: FAIL · clean rate 0.70 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
106
+
107
+ $ faultgate check examples/gate/baseline_glm_run2.json --band band.json
108
+ gate: PASS · clean rate 0.93 vs band [0.93, 0.93] from 2 baseline runs · fail below 0.83
109
+ ```
110
+
111
+ Real runs: two `z-ai/glm-5.3-flash` baselines and `openai/gpt-5.6-luna` / `qwen/qwen3.7-flash` candidates on the same 30 multi-hop scenarios. The verdicts match FAULTLINE P11's live drill (R4 FAIL / R1 FAIL / R2 PASS), which cost $1.84 and an API judge; this took 0.0 s and no model.
112
+
113
+ | Verdict | Rule (n = scenarios in the band) | Exit |
114
+ |---|---|---|
115
+ | **PASS** | clean rate ≥ min observed − 1/n | 0 |
116
+ | **WARN** | in between — a human should look | 0 |
117
+ | **FAIL** | clean rate < min observed − 3/n | 1 |
118
+
119
+ It also lists **regressions**: scenarios clean in *every* baseline run and not clean now — the ones to read first. Scenarios are matched across runs by `faultgate.scenario`, else `gen_ai.agent.id`, else a hash of the prompt.
120
+
121
+ With expected answers the gate grades correctness too, and the numbers land on P11's exactly:
122
+
123
+ ```
124
+ $ faultgate baseline examples/gate/baseline_glm_run*.json --golden examples/gate/golden.json --out band.json
125
+ examples/gate/baseline_glm_run1.json: 30 traces · clean rate 0.80 · 0.0s # P11: 24/30 grounded
126
+ examples/gate/baseline_glm_run2.json: 30 traces · clean rate 0.87 · 0.0s # P11: 26/30 grounded
127
+ band [0.80, 0.87] over 30 scenarios × 2 runs · 21 clean in every run
128
+
129
+ $ faultgate check examples/gate/candidate_gpt.json --golden examples/gate/golden.json --band band.json
130
+ 21/30 traces failed · search_loop=18, abstention=0, policy=0, wrong_answer=3, truncation=3
131
+ gate: FAIL · clean rate 0.30 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70
132
+
133
+ $ faultgate check examples/gate/candidate_qwen.json --golden examples/gate/golden.json --band band.json
134
+ 18/30 traces failed · search_loop=9, abstention=0, policy=0, wrong_answer=9, truncation=8
135
+ gate: FAIL · clean rate 0.40 vs band [0.80, 0.87] from 2 baseline runs · fail below 0.70
136
+ ```
137
+
138
+ (Qwen returns the head of the answer token without its suffix in 8 of 30 runs. Nobody had noticed.)
139
+
140
+ ## In CI
141
+
142
+ ```yaml
143
+ - uses: samirsawarkar/faultgate@main
144
+ with:
145
+ traces: traces/candidate.json # exported by your agent's OTel pipeline
146
+ band: traces/band.json # from `faultgate baseline`, committed
147
+ golden: traces/golden.json # optional
148
+ policy: traces/policy.json # optional
149
+ ```
150
+
151
+ Exit 1 on gate FAIL, so the job fails; WARN and PASS pass. The report lands in `faultgate-report.json` for upload as an artifact. The action installs nothing heavier than pydantic; our own CI runs it against this repo's examples and asserts that the GPT candidate fails and the GLM baseline passes.
152
+
153
+ ## How much to trust a verdict
154
+
155
+ This is the part every evaluation tool skips. Every check and judge here is scored against **human failure-mode labels** on 200 real agent runs (FAULTLINE P4), reported on all 200 and on P5's frozen test split (n = 60). Regenerate with `scripts/calibrate.py`.
156
+
157
+ | Check | Kind | Positives | TPR | TNR | κ (n=200) | κ (P5 test, n=60) |
158
+ |---|---|---|---|---|---|---|
159
+ | `search_loop` | rule | 36 | 1.00 | 0.99 | **0.98** | **1.00** |
160
+ | `abstention` | rule | 1 | 1.00 | 1.00 | 1.00 | — (0 positives) |
161
+ | `policy` | rule | synthetic | 4/4 P9 categories | 0 FP on 290 real traces | — | — |
162
+ | `wrong_answer` | golden | 4 | 1.00 | 1.00 | 1.00 | 1.00 |
163
+ | `truncation` | golden | 1 | 1.00 | 1.00 | 1.00 | — (0 positives) |
164
+ | `wrong_direction` | Laya judge | 1 | 1.00 | 0.61 | 0.02 | 0.00 |
165
+
166
+ For comparison, the same `search_loop` question put to judges instead of a rule: Laya κ 0.09, GLM-5.3 (API) κ −0.03. **Where a failure is structural, code beats every model we measured**, which is why the default checks are rules.
167
+
168
+ Read the table honestly:
169
+ - `search_loop` is solid on this task family. The human coder saw the same structural signals, so κ measures agreement with that coding, not ground truth from nowhere.
170
+ - `abstention` and `truncation` have one positive each to learn from. `abstention` is a phrase list; a paraphrase it doesn't know is a miss.
171
+ - `wrong_answer` agrees with the human verdict on all 21 answered runs. It is substring containment, case-insensitive: right for short factual answers, wrong for free-text ones — say what "expected" means for your task before you trust it.
172
+ - `policy` has no human-labelled positives: FAULTLINE's agent rejects hostile calls before they become steps, so its traces never contain an executed attack. Recall is by construction on the P9 attack shapes; the number that matters is zero false positives on real runs.
173
+ - `wrong_direction` is the only check that needs reading, and the only local judge we have is noise on it (fires on 39% of clean runs). It ships because the contract is the product: the day a local model scores κ ≥ 0.7 on it, that's a one-line adapter, and this harness is what proves it.
174
+ - Laya's context is 512 tokens. `faultgate` compacts the trace to fit (answer and termination first, middle steps elided) and counts every call that still overflows; the count is printed.
175
+
176
+ Full confusion matrices and Wilson CIs for the original P5 judges: [FAULTLINE `projects/p05_judge/DECISIONS.md`](https://github.com/samirsawarkar/faultline-ai-reliability/blob/main/projects/p05_judge/DECISIONS.md).
177
+
178
+ ## Swap the judge
179
+
180
+ ```python
181
+ class Judge(Protocol):
182
+ name: str
183
+ budget: int | None # max characters of trace it can read
184
+ def ask(self, state: str, question: str) -> Verdict: ... # Verdict(detected, confidence, reason)
185
+ ```
186
+
187
+ `laya` and `litellm:<model>` ship in the box. Add a judge, run `scripts/calibrate.py --judge yours`, and publish the row.
188
+
189
+ ## What it doesn't do yet
190
+
191
+ - **Premature stop** (answered with an intermediate entity of the chain). Needs the traversal chain, not just the final answer.
192
+ - **Semantic injection checks** (the agent obeyed text it read). P9 has the taxonomy; needs a judge that scores.
193
+ - **Running your agent.** `faultgate` reads traces; it never executes anything.
194
+
195
+ ## Built on FAULTLINE
196
+
197
+ [FAULTLINE](https://github.com/samirsawarkar/faultline-ai-reliability) is the research workbench behind this tool: 30 days of simulator, 16 measured projects, two papers. `faultgate` is the part you install. Every number in this README links back to the run that produced it.
198
+
199
+ MIT © 2026 Samir Sawarkar
@@ -0,0 +1,77 @@
1
+ {
2
+ "judge": "none",
3
+ "checks": [
4
+ "search_loop",
5
+ "abstention",
6
+ "policy",
7
+ "wrong_answer",
8
+ "truncation"
9
+ ],
10
+ "runs": 2,
11
+ "n": 30,
12
+ "scenarios": [
13
+ "r-0204",
14
+ "r-0205",
15
+ "r-0208",
16
+ "r-0214",
17
+ "r-0216",
18
+ "r-0221",
19
+ "r-0224",
20
+ "r-0226",
21
+ "r-0232",
22
+ "r-0242",
23
+ "r-0258",
24
+ "r-0263",
25
+ "r-0274",
26
+ "r-0277",
27
+ "r-0279",
28
+ "r-0286",
29
+ "r-0292",
30
+ "r-0300",
31
+ "r-0307",
32
+ "r-0317",
33
+ "r-0318",
34
+ "r-0320",
35
+ "r-0322",
36
+ "r-0324",
37
+ "r-0330",
38
+ "r-0332",
39
+ "r-0336",
40
+ "r-0344",
41
+ "r-0349",
42
+ "r-0350"
43
+ ],
44
+ "clean_rate": {
45
+ "min": 0.8,
46
+ "max": 0.8666666666666667,
47
+ "mean": 0.8333333333333334,
48
+ "per_run": [
49
+ 0.8,
50
+ 0.8666666666666667
51
+ ]
52
+ },
53
+ "clean_in_all_runs": [
54
+ "r-0204",
55
+ "r-0205",
56
+ "r-0214",
57
+ "r-0216",
58
+ "r-0226",
59
+ "r-0242",
60
+ "r-0258",
61
+ "r-0279",
62
+ "r-0286",
63
+ "r-0292",
64
+ "r-0307",
65
+ "r-0317",
66
+ "r-0318",
67
+ "r-0320",
68
+ "r-0322",
69
+ "r-0324",
70
+ "r-0330",
71
+ "r-0332",
72
+ "r-0344",
73
+ "r-0349",
74
+ "r-0350"
75
+ ],
76
+ "rule": "PASS if clean_rate >= min - 1/n; FAIL if clean_rate < min - 3/n; else WARN"
77
+ }