openapi-agent-ready 0.3.0__tar.gz → 0.4.1__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.
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/CHANGELOG.md +25 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/PKG-INFO +66 -35
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/README.md +64 -33
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/pyproject.toml +1 -1
- openapi_agent_ready-0.4.1/src/agent_ready/baseline.py +137 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/cli.py +95 -4
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/rubric.py +4 -3
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/tests/test_audit.py +7 -0
- openapi_agent_ready-0.4.1/tests/test_baseline.py +306 -0
- openapi_agent_ready-0.3.0/openapi.yaml +0 -11890
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/.github/workflows/ci.yml +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/.github/workflows/publish.yml +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/.gitignore +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/CONTRIBUTING.md +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/LICENSE +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/examples/agent_ready_booking_api.yaml +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/examples/sample_booking_api.yaml +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/__init__.py +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/auditor.py +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/mcp_generator.py +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/report_generator.py +0 -0
- {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/tests/test_mcp_conformance.py +0 -0
|
@@ -5,6 +5,31 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-10-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Swagger 2.0 specs are now rejected (exit 2)** instead of being audited.
|
|
12
|
+
They have `paths`, so they were scored, but auth lives in
|
|
13
|
+
`securityDefinitions`, so a spec with scoped OAuth2 was reported as having no
|
|
14
|
+
security schemes at all.
|
|
15
|
+
- **Progress bar counts operations, not paths.** A spec with 100 paths × 3
|
|
16
|
+
methods (300 operations) previously never showed progress.
|
|
17
|
+
|
|
18
|
+
## [0.4.0] - 2026-08-08
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Baseline mode** (`--write-baseline`, `--baseline`, `--max-new`). A mature
|
|
22
|
+
API audited for the first time produces hundreds of findings, so a score gate
|
|
23
|
+
is unusable on day one and gets switched off. Recording the current state lets
|
|
24
|
+
CI block only regressions while the backlog stays visible.
|
|
25
|
+
|
|
26
|
+
Findings are matched on category, endpoint, and severity rather than message
|
|
27
|
+
text — messages embed counts and parameter names ("3/3 parameters have no
|
|
28
|
+
description (a, b, c)") that shift when unrelated things change, which would
|
|
29
|
+
report unfixed problems as new. Severity is part of the identity so a warning
|
|
30
|
+
escalating to a fail is caught; message is excluded so rewording a finding in
|
|
31
|
+
a future release doesn't invalidate stored baselines.
|
|
32
|
+
|
|
8
33
|
## [0.3.0] - 2026-07-31
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: openapi-agent-ready
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Audit any OpenAPI spec for AI-agent and MCP readiness, and generate an MCP server scaffold.
|
|
5
5
|
Project-URL: Homepage, https://github.com/sfaisal/agent-ready
|
|
6
6
|
Project-URL: Issues, https://github.com/sfaisal/agent-ready/issues
|
|
@@ -64,7 +64,7 @@ Description-Content-Type: text/markdown
|
|
|
64
64
|
**Audit any OpenAPI spec for AI-agent readiness — then generate an MCP server scaffold for the endpoints that pass.**
|
|
65
65
|
|
|
66
66
|
Your API works fine for human developers. That doesn't mean an AI agent can use
|
|
67
|
-
it reliably. `agent-ready` scores a spec against
|
|
67
|
+
it reliably. `agent-ready` scores a spec against eight categories of
|
|
68
68
|
agent-specific failure, tells you exactly which endpoints will cause trouble and
|
|
69
69
|
why, and can gate a CI pipeline so specs don't regress.
|
|
70
70
|
|
|
@@ -75,17 +75,18 @@ agent-ready https://example.com/openapi.yaml
|
|
|
75
75
|
|
|
76
76
|
```
|
|
77
77
|
Generic Booking Platform API (v1.0)
|
|
78
|
-
AI-readiness score:
|
|
79
|
-
Endpoints: 4 Fails:
|
|
78
|
+
AI-readiness score: 58.4/100
|
|
79
|
+
Endpoints: 4 Fails: 7 Warnings: 14
|
|
80
80
|
|
|
81
81
|
Category scores:
|
|
82
|
-
Description Clarity
|
|
83
|
-
Side Effects
|
|
84
|
-
Error Responses
|
|
85
|
-
Auth Clarity
|
|
86
|
-
Ambiguity
|
|
87
|
-
Parameter Explanation
|
|
88
|
-
Usage Guidelines
|
|
82
|
+
Description Clarity 37.5/100
|
|
83
|
+
Side Effects 70.0/100
|
|
84
|
+
Error Responses 62.5/100
|
|
85
|
+
Auth Clarity 50.0/100
|
|
86
|
+
Ambiguity 100.0/100
|
|
87
|
+
Parameter Explanation 10.0/100
|
|
88
|
+
Usage Guidelines 50.0/100
|
|
89
|
+
Tool Surface 100.0/100
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
## Why this exists
|
|
@@ -111,11 +112,8 @@ directly onto the rubric:
|
|
|
111
112
|
| **Frictionless authentication** | 4 (auth/scope clarity) |
|
|
112
113
|
| **Structured error handling** | 3 (actionable errors) |
|
|
113
114
|
|
|
114
|
-
Categories 2 (side effects) and 5 (ambiguity)
|
|
115
|
-
|
|
116
|
-
unsignalled write operations and confusable endpoint descriptions cause agent
|
|
117
|
-
failures as often as the original three. Category 8 came later still, from
|
|
118
|
-
OpenAI's function-calling guidance.
|
|
115
|
+
Categories 2 (side effects) and 5 (ambiguity) weren't in the article. They came
|
|
116
|
+
up once I started building, and they matter as much as the original three.
|
|
119
117
|
|
|
120
118
|
The gap is measurable, and enormous. A 2026 study of 856 tools across 103 MCP
|
|
121
119
|
servers found that **97.1% of tool descriptions contained at least one quality
|
|
@@ -185,9 +183,55 @@ Exit codes:
|
|
|
185
183
|
| Code | Meaning |
|
|
186
184
|
|---|---|
|
|
187
185
|
| `0` | All configured gates passed |
|
|
188
|
-
| `1` | A gate failed (`--min-score` / `--max-fails`) |
|
|
186
|
+
| `1` | A gate failed (`--min-score` / `--max-fails` / `--max-new`) |
|
|
189
187
|
| `2` | The spec could not be loaded or parsed |
|
|
190
188
|
|
|
189
|
+
### Adopting on an existing API
|
|
190
|
+
|
|
191
|
+
A mature API audited for the first time will produce hundreds of findings. No
|
|
192
|
+
team can fix all of them before merging anything, so a score gate is unusable
|
|
193
|
+
on day one and gets switched off within a week.
|
|
194
|
+
|
|
195
|
+
Record the current state instead, and block only what gets worse:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# once, when you adopt the tool — commit this file
|
|
199
|
+
agent-ready openapi.yaml --write-baseline .agent-ready-baseline.json
|
|
200
|
+
|
|
201
|
+
# in CI, from then on
|
|
202
|
+
agent-ready openapi.yaml --baseline .agent-ready-baseline.json --max-new 0
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Existing findings stay visible in the report but don't fail the build. A pull
|
|
206
|
+
request that introduces a new one does, and the output names exactly which
|
|
207
|
+
findings it added:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
Compared against .agent-ready-baseline.json: 5 new, 0 fixed, 11 unchanged
|
|
211
|
+
NEW FAIL GET /invoices — Description is a bare label (12 chars) ...
|
|
212
|
+
NEW FAIL GET /invoices — No error responses documented ...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Findings are matched on category, endpoint, and severity — not on message text,
|
|
216
|
+
which contains counts and parameter names that shift when unrelated things
|
|
217
|
+
change. A warning escalating to a fail counts as new; rewording a finding in a
|
|
218
|
+
future release does not invalidate your baseline.
|
|
219
|
+
|
|
220
|
+
As the team fixes the backlog, regenerate the baseline to lock in the progress.
|
|
221
|
+
|
|
222
|
+
### Python API
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from agent_ready import load_spec, run_audit
|
|
226
|
+
|
|
227
|
+
spec = load_spec("openapi.yaml")
|
|
228
|
+
result = run_audit(spec)
|
|
229
|
+
|
|
230
|
+
print(result["overall_score"])
|
|
231
|
+
for finding in result["gaps"]:
|
|
232
|
+
print(finding.severity, finding.endpoint, finding.message)
|
|
233
|
+
```
|
|
234
|
+
|
|
191
235
|
## Real-world results
|
|
192
236
|
|
|
193
237
|
Running it against Stripe's production spec (587 endpoints), as of v0.1.0.
|
|
@@ -213,8 +257,8 @@ Two things worth drawing out.
|
|
|
213
257
|
in this tool, not in Stripe.** Stripe documents errors via OpenAPI's `default`
|
|
214
258
|
response rather than enumerating explicit 4xx/5xx codes. Perfectly valid, and
|
|
215
259
|
arguably cleaner. The check only looked for `4`/`5` prefixes, so it failed all
|
|
216
|
-
587 endpoints. Fixed
|
|
217
|
-
|
|
260
|
+
587 endpoints. Fixed and regression-tested — and a good reason to run this
|
|
261
|
+
against your own real spec before trusting the score.
|
|
218
262
|
|
|
219
263
|
**Stripe is widely and correctly regarded as agent-ready, yet its raw OpenAPI
|
|
220
264
|
spec scores 62.9.** That isn't a contradiction — it's the whole point. Stripe's
|
|
@@ -229,19 +273,6 @@ Genuine findings that survived scrutiny: `GET /v1/account` and
|
|
|
229
273
|
uses bearer/basic API keys rather than scoped OAuth2, so there's no way to
|
|
230
274
|
express "this agent may only read."
|
|
231
275
|
|
|
232
|
-
### Python API
|
|
233
|
-
|
|
234
|
-
```python
|
|
235
|
-
from agent_ready import load_spec, run_audit
|
|
236
|
-
|
|
237
|
-
spec = load_spec("openapi.yaml")
|
|
238
|
-
result = run_audit(spec)
|
|
239
|
-
|
|
240
|
-
print(result["overall_score"])
|
|
241
|
-
for finding in result["gaps"]:
|
|
242
|
-
print(finding.severity, finding.endpoint, finding.message)
|
|
243
|
-
```
|
|
244
|
-
|
|
245
276
|
## The eight categories
|
|
246
277
|
|
|
247
278
|
| # | Category | What it catches | Published anchor |
|
|
@@ -305,7 +336,7 @@ doesn't:
|
|
|
305
336
|
|
|
306
337
|
- **The ambiguity check uses string similarity**, so it misses endpoints that
|
|
307
338
|
are semantically similar but differently worded. Embedding-based similarity
|
|
308
|
-
would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues))
|
|
339
|
+
would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues/1))
|
|
309
340
|
- **Category weights are judgement calls, not measured effect sizes.**
|
|
310
341
|
Description clarity is weighted highest on the prior that wrong tool selection
|
|
311
342
|
is the costliest failure — that prior is untested here.
|
|
@@ -328,7 +359,7 @@ doesn't:
|
|
|
328
359
|
- [ ] LLM-assisted rewriting of flagged descriptions (`--fix`)
|
|
329
360
|
- [ ] Live tool-selection testing against a generated MCP server
|
|
330
361
|
- [ ] GitHub Action wrapper
|
|
331
|
-
- [
|
|
362
|
+
- [x] Baseline mode (`--baseline`) so CI only fails on *new* regressions
|
|
332
363
|
|
|
333
364
|
## Contributing
|
|
334
365
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
**Audit any OpenAPI spec for AI-agent readiness — then generate an MCP server scaffold for the endpoints that pass.**
|
|
10
10
|
|
|
11
11
|
Your API works fine for human developers. That doesn't mean an AI agent can use
|
|
12
|
-
it reliably. `agent-ready` scores a spec against
|
|
12
|
+
it reliably. `agent-ready` scores a spec against eight categories of
|
|
13
13
|
agent-specific failure, tells you exactly which endpoints will cause trouble and
|
|
14
14
|
why, and can gate a CI pipeline so specs don't regress.
|
|
15
15
|
|
|
@@ -20,17 +20,18 @@ agent-ready https://example.com/openapi.yaml
|
|
|
20
20
|
|
|
21
21
|
```
|
|
22
22
|
Generic Booking Platform API (v1.0)
|
|
23
|
-
AI-readiness score:
|
|
24
|
-
Endpoints: 4 Fails:
|
|
23
|
+
AI-readiness score: 58.4/100
|
|
24
|
+
Endpoints: 4 Fails: 7 Warnings: 14
|
|
25
25
|
|
|
26
26
|
Category scores:
|
|
27
|
-
Description Clarity
|
|
28
|
-
Side Effects
|
|
29
|
-
Error Responses
|
|
30
|
-
Auth Clarity
|
|
31
|
-
Ambiguity
|
|
32
|
-
Parameter Explanation
|
|
33
|
-
Usage Guidelines
|
|
27
|
+
Description Clarity 37.5/100
|
|
28
|
+
Side Effects 70.0/100
|
|
29
|
+
Error Responses 62.5/100
|
|
30
|
+
Auth Clarity 50.0/100
|
|
31
|
+
Ambiguity 100.0/100
|
|
32
|
+
Parameter Explanation 10.0/100
|
|
33
|
+
Usage Guidelines 50.0/100
|
|
34
|
+
Tool Surface 100.0/100
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
## Why this exists
|
|
@@ -56,11 +57,8 @@ directly onto the rubric:
|
|
|
56
57
|
| **Frictionless authentication** | 4 (auth/scope clarity) |
|
|
57
58
|
| **Structured error handling** | 3 (actionable errors) |
|
|
58
59
|
|
|
59
|
-
Categories 2 (side effects) and 5 (ambiguity)
|
|
60
|
-
|
|
61
|
-
unsignalled write operations and confusable endpoint descriptions cause agent
|
|
62
|
-
failures as often as the original three. Category 8 came later still, from
|
|
63
|
-
OpenAI's function-calling guidance.
|
|
60
|
+
Categories 2 (side effects) and 5 (ambiguity) weren't in the article. They came
|
|
61
|
+
up once I started building, and they matter as much as the original three.
|
|
64
62
|
|
|
65
63
|
The gap is measurable, and enormous. A 2026 study of 856 tools across 103 MCP
|
|
66
64
|
servers found that **97.1% of tool descriptions contained at least one quality
|
|
@@ -130,9 +128,55 @@ Exit codes:
|
|
|
130
128
|
| Code | Meaning |
|
|
131
129
|
|---|---|
|
|
132
130
|
| `0` | All configured gates passed |
|
|
133
|
-
| `1` | A gate failed (`--min-score` / `--max-fails`) |
|
|
131
|
+
| `1` | A gate failed (`--min-score` / `--max-fails` / `--max-new`) |
|
|
134
132
|
| `2` | The spec could not be loaded or parsed |
|
|
135
133
|
|
|
134
|
+
### Adopting on an existing API
|
|
135
|
+
|
|
136
|
+
A mature API audited for the first time will produce hundreds of findings. No
|
|
137
|
+
team can fix all of them before merging anything, so a score gate is unusable
|
|
138
|
+
on day one and gets switched off within a week.
|
|
139
|
+
|
|
140
|
+
Record the current state instead, and block only what gets worse:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# once, when you adopt the tool — commit this file
|
|
144
|
+
agent-ready openapi.yaml --write-baseline .agent-ready-baseline.json
|
|
145
|
+
|
|
146
|
+
# in CI, from then on
|
|
147
|
+
agent-ready openapi.yaml --baseline .agent-ready-baseline.json --max-new 0
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Existing findings stay visible in the report but don't fail the build. A pull
|
|
151
|
+
request that introduces a new one does, and the output names exactly which
|
|
152
|
+
findings it added:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Compared against .agent-ready-baseline.json: 5 new, 0 fixed, 11 unchanged
|
|
156
|
+
NEW FAIL GET /invoices — Description is a bare label (12 chars) ...
|
|
157
|
+
NEW FAIL GET /invoices — No error responses documented ...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Findings are matched on category, endpoint, and severity — not on message text,
|
|
161
|
+
which contains counts and parameter names that shift when unrelated things
|
|
162
|
+
change. A warning escalating to a fail counts as new; rewording a finding in a
|
|
163
|
+
future release does not invalidate your baseline.
|
|
164
|
+
|
|
165
|
+
As the team fixes the backlog, regenerate the baseline to lock in the progress.
|
|
166
|
+
|
|
167
|
+
### Python API
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from agent_ready import load_spec, run_audit
|
|
171
|
+
|
|
172
|
+
spec = load_spec("openapi.yaml")
|
|
173
|
+
result = run_audit(spec)
|
|
174
|
+
|
|
175
|
+
print(result["overall_score"])
|
|
176
|
+
for finding in result["gaps"]:
|
|
177
|
+
print(finding.severity, finding.endpoint, finding.message)
|
|
178
|
+
```
|
|
179
|
+
|
|
136
180
|
## Real-world results
|
|
137
181
|
|
|
138
182
|
Running it against Stripe's production spec (587 endpoints), as of v0.1.0.
|
|
@@ -158,8 +202,8 @@ Two things worth drawing out.
|
|
|
158
202
|
in this tool, not in Stripe.** Stripe documents errors via OpenAPI's `default`
|
|
159
203
|
response rather than enumerating explicit 4xx/5xx codes. Perfectly valid, and
|
|
160
204
|
arguably cleaner. The check only looked for `4`/`5` prefixes, so it failed all
|
|
161
|
-
587 endpoints. Fixed
|
|
162
|
-
|
|
205
|
+
587 endpoints. Fixed and regression-tested — and a good reason to run this
|
|
206
|
+
against your own real spec before trusting the score.
|
|
163
207
|
|
|
164
208
|
**Stripe is widely and correctly regarded as agent-ready, yet its raw OpenAPI
|
|
165
209
|
spec scores 62.9.** That isn't a contradiction — it's the whole point. Stripe's
|
|
@@ -174,19 +218,6 @@ Genuine findings that survived scrutiny: `GET /v1/account` and
|
|
|
174
218
|
uses bearer/basic API keys rather than scoped OAuth2, so there's no way to
|
|
175
219
|
express "this agent may only read."
|
|
176
220
|
|
|
177
|
-
### Python API
|
|
178
|
-
|
|
179
|
-
```python
|
|
180
|
-
from agent_ready import load_spec, run_audit
|
|
181
|
-
|
|
182
|
-
spec = load_spec("openapi.yaml")
|
|
183
|
-
result = run_audit(spec)
|
|
184
|
-
|
|
185
|
-
print(result["overall_score"])
|
|
186
|
-
for finding in result["gaps"]:
|
|
187
|
-
print(finding.severity, finding.endpoint, finding.message)
|
|
188
|
-
```
|
|
189
|
-
|
|
190
221
|
## The eight categories
|
|
191
222
|
|
|
192
223
|
| # | Category | What it catches | Published anchor |
|
|
@@ -250,7 +281,7 @@ doesn't:
|
|
|
250
281
|
|
|
251
282
|
- **The ambiguity check uses string similarity**, so it misses endpoints that
|
|
252
283
|
are semantically similar but differently worded. Embedding-based similarity
|
|
253
|
-
would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues))
|
|
284
|
+
would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues/1))
|
|
254
285
|
- **Category weights are judgement calls, not measured effect sizes.**
|
|
255
286
|
Description clarity is weighted highest on the prior that wrong tool selection
|
|
256
287
|
is the costliest failure — that prior is untested here.
|
|
@@ -273,7 +304,7 @@ doesn't:
|
|
|
273
304
|
- [ ] LLM-assisted rewriting of flagged descriptions (`--fix`)
|
|
274
305
|
- [ ] Live tool-selection testing against a generated MCP server
|
|
275
306
|
- [ ] GitHub Action wrapper
|
|
276
|
-
- [
|
|
307
|
+
- [x] Baseline mode (`--baseline`) so CI only fails on *new* regressions
|
|
277
308
|
|
|
278
309
|
## Contributing
|
|
279
310
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openapi-agent-ready"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Audit any OpenAPI spec for AI-agent and MCP readiness, and generate an MCP server scaffold."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Baseline support: fail CI only on *new* findings.
|
|
3
|
+
|
|
4
|
+
The problem this solves: a mature API audited for the first time produces
|
|
5
|
+
hundreds of findings. A team cannot fix them all before merging anything, so
|
|
6
|
+
`--min-score` and `--max-fails` are unusable as gates and get switched off
|
|
7
|
+
within a week. A baseline records the current state and lets the gate fail only
|
|
8
|
+
when something gets worse.
|
|
9
|
+
|
|
10
|
+
The whole design rests on one question: what makes a finding "the same finding"
|
|
11
|
+
across two runs? Message text is the obvious answer and the wrong one — it
|
|
12
|
+
embeds counts and parameter names ("3/3 parameters have no description
|
|
13
|
+
(date_from, date_to, location)") that change when an unrelated parameter is
|
|
14
|
+
added, which would report a fixed finding as new.
|
|
15
|
+
|
|
16
|
+
So findings are fingerprinted on (category, endpoint, severity) instead:
|
|
17
|
+
|
|
18
|
+
- **category** — which rule fired.
|
|
19
|
+
- **endpoint** — where. Stable across edits to the endpoint's content.
|
|
20
|
+
- **severity** — a warning becoming a fail is a regression worth catching, so
|
|
21
|
+
it belongs in the identity rather than being treated as the same finding.
|
|
22
|
+
|
|
23
|
+
Message is deliberately excluded. Rewording a finding's text in a future
|
|
24
|
+
release must not invalidate every stored baseline.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from collections import Counter
|
|
29
|
+
from dataclasses import asdict
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
BASELINE_VERSION = 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaselineError(Exception):
|
|
36
|
+
"""Raised when a baseline file can't be read or is malformed."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fingerprint(finding) -> str:
|
|
40
|
+
"""A stable identity for a finding across runs. See module docstring."""
|
|
41
|
+
return f"{finding.category}|{finding.endpoint}|{finding.severity}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _counted(findings) -> Counter:
|
|
45
|
+
"""
|
|
46
|
+
Fingerprints with multiplicity.
|
|
47
|
+
|
|
48
|
+
A single endpoint can legitimately produce two findings in one category —
|
|
49
|
+
for example a parameter-explanation fail for undocumented parameters plus a
|
|
50
|
+
warning for a missing date format. Counting rather than set-membership means
|
|
51
|
+
going from one such finding to two is correctly reported as new.
|
|
52
|
+
"""
|
|
53
|
+
return Counter(fingerprint(f) for f in findings)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def write_baseline(path: str, spec: dict, result: dict) -> dict:
|
|
57
|
+
"""Record the current findings so future runs can compare against them."""
|
|
58
|
+
info = spec.get("info") or {}
|
|
59
|
+
payload = {
|
|
60
|
+
"baseline_version": BASELINE_VERSION,
|
|
61
|
+
"api": {
|
|
62
|
+
"title": info.get("title", "Untitled API"),
|
|
63
|
+
"version": str(info.get("version", "unknown")),
|
|
64
|
+
},
|
|
65
|
+
"overall_score": result["overall_score"],
|
|
66
|
+
"endpoint_count": result["endpoint_count"],
|
|
67
|
+
"fingerprints": sorted(_counted(result["gaps"]).elements()),
|
|
68
|
+
# Stored for human readability when reviewing a baseline diff; never
|
|
69
|
+
# read back during comparison.
|
|
70
|
+
"findings": [asdict(f) for f in result["gaps"]],
|
|
71
|
+
}
|
|
72
|
+
Path(path).write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
73
|
+
return payload
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_baseline(path: str) -> dict:
|
|
77
|
+
p = Path(path)
|
|
78
|
+
if not p.exists():
|
|
79
|
+
raise BaselineError(
|
|
80
|
+
f"Baseline file not found: {path}\n"
|
|
81
|
+
f"Create one from the current state with: --write-baseline {path}"
|
|
82
|
+
)
|
|
83
|
+
try:
|
|
84
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
85
|
+
except json.JSONDecodeError as exc:
|
|
86
|
+
raise BaselineError(f"Baseline file is not valid JSON: {exc}") from exc
|
|
87
|
+
|
|
88
|
+
if not isinstance(data, dict) or "fingerprints" not in data:
|
|
89
|
+
raise BaselineError(
|
|
90
|
+
f"{path} does not look like an agent-ready baseline "
|
|
91
|
+
"(no 'fingerprints' key)."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
version = data.get("baseline_version", 0)
|
|
95
|
+
if version > BASELINE_VERSION:
|
|
96
|
+
raise BaselineError(
|
|
97
|
+
f"Baseline was written by a newer version of agent-ready "
|
|
98
|
+
f"(format v{version}, this build understands v{BASELINE_VERSION}). "
|
|
99
|
+
"Upgrade agent-ready or regenerate the baseline."
|
|
100
|
+
)
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def compare_to_baseline(result: dict, baseline: dict) -> dict:
|
|
105
|
+
"""
|
|
106
|
+
Classify current findings against a baseline.
|
|
107
|
+
|
|
108
|
+
Returns counts plus the actual new findings, so the CLI can show a team
|
|
109
|
+
exactly what their change introduced rather than the whole backlog.
|
|
110
|
+
"""
|
|
111
|
+
baseline_counts = Counter(baseline.get("fingerprints", []))
|
|
112
|
+
current_counts = _counted(result["gaps"])
|
|
113
|
+
|
|
114
|
+
# A fingerprint appearing more often than the baseline recorded is new,
|
|
115
|
+
# by the excess amount.
|
|
116
|
+
new_counts = current_counts - baseline_counts
|
|
117
|
+
fixed_counts = baseline_counts - current_counts
|
|
118
|
+
|
|
119
|
+
# Map fingerprints back to findings so the report can name them. Consume
|
|
120
|
+
# the allowance so duplicates beyond it are still reported.
|
|
121
|
+
remaining = Counter(new_counts)
|
|
122
|
+
new_findings = []
|
|
123
|
+
for f in result["gaps"]:
|
|
124
|
+
fp = fingerprint(f)
|
|
125
|
+
if remaining.get(fp, 0) > 0:
|
|
126
|
+
new_findings.append(f)
|
|
127
|
+
remaining[fp] -= 1
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"new_findings": new_findings,
|
|
131
|
+
"new_count": sum(new_counts.values()),
|
|
132
|
+
"fixed_count": sum(fixed_counts.values()),
|
|
133
|
+
"unchanged_count": sum((current_counts & baseline_counts).values()),
|
|
134
|
+
"new_fails": sum(1 for f in new_findings if f.severity == "fail"),
|
|
135
|
+
"baseline_score": baseline.get("overall_score"),
|
|
136
|
+
"current_score": result["overall_score"],
|
|
137
|
+
}
|
|
@@ -3,8 +3,8 @@ Command-line interface for agent-ready.
|
|
|
3
3
|
|
|
4
4
|
Exit codes (designed for CI use):
|
|
5
5
|
0 audit passed all configured gates
|
|
6
|
-
1 audit ran, but a gate failed (score below --min-score,
|
|
7
|
-
--max-fails exceeded)
|
|
6
|
+
1 audit ran, but a gate failed (score below --min-score,
|
|
7
|
+
--max-fails exceeded, or --max-new exceeded against --baseline)
|
|
8
8
|
2 the spec could not be loaded or parsed
|
|
9
9
|
"""
|
|
10
10
|
|
|
@@ -17,10 +17,17 @@ from . import __version__
|
|
|
17
17
|
from .auditor import (
|
|
18
18
|
SpecLoadError,
|
|
19
19
|
endpoints_ready_for_mcp,
|
|
20
|
+
extract_endpoints,
|
|
20
21
|
load_spec,
|
|
21
22
|
result_to_dict,
|
|
22
23
|
run_audit,
|
|
23
24
|
)
|
|
25
|
+
from .baseline import (
|
|
26
|
+
BaselineError,
|
|
27
|
+
compare_to_baseline,
|
|
28
|
+
load_baseline,
|
|
29
|
+
write_baseline,
|
|
30
|
+
)
|
|
24
31
|
from .mcp_generator import generate_mcp_scaffold
|
|
25
32
|
from .report_generator import generate_report
|
|
26
33
|
|
|
@@ -38,7 +45,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
38
45
|
),
|
|
39
46
|
epilog=(
|
|
40
47
|
"CI example:\n"
|
|
41
|
-
" agent-ready openapi.yaml --min-score
|
|
48
|
+
" agent-ready openapi.yaml --min-score 60 --max-fails 0 --quiet\n"
|
|
49
|
+
"\n"
|
|
50
|
+
"Adopting on an existing API, where fixing everything first is not\n"
|
|
51
|
+
"realistic — record today's findings, then block only regressions:\n"
|
|
52
|
+
" agent-ready openapi.yaml --write-baseline .agent-ready-baseline.json\n"
|
|
53
|
+
" agent-ready openapi.yaml --baseline .agent-ready-baseline.json --max-new 0\n"
|
|
42
54
|
),
|
|
43
55
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
44
56
|
)
|
|
@@ -60,6 +72,31 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
60
72
|
type=int,
|
|
61
73
|
help="Exit 1 if the number of FAIL findings exceeds this value (CI gate)",
|
|
62
74
|
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--write-baseline",
|
|
77
|
+
metavar="PATH",
|
|
78
|
+
help=(
|
|
79
|
+
"Record current findings to PATH as an accepted baseline. Run this "
|
|
80
|
+
"once when adopting the tool on an existing API."
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--baseline",
|
|
85
|
+
metavar="PATH",
|
|
86
|
+
help=(
|
|
87
|
+
"Compare against a baseline file and report what changed. Combine "
|
|
88
|
+
"with --max-new to fail only on regressions."
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--max-new",
|
|
93
|
+
type=int,
|
|
94
|
+
metavar="N",
|
|
95
|
+
help=(
|
|
96
|
+
"Exit 1 if more than N findings are new since --baseline. "
|
|
97
|
+
"Use --max-new 0 to block any regression."
|
|
98
|
+
),
|
|
99
|
+
)
|
|
63
100
|
parser.add_argument(
|
|
64
101
|
"--generate-mcp",
|
|
65
102
|
metavar="PATH",
|
|
@@ -147,6 +184,16 @@ def _run(argv=None) -> int:
|
|
|
147
184
|
print(f"error: {exc}", file=sys.stderr)
|
|
148
185
|
return EXIT_SPEC_ERROR
|
|
149
186
|
|
|
187
|
+
# Swagger 2.0 has `paths` too, but keeps auth in `securityDefinitions` and
|
|
188
|
+
# bodies in `in: body` params, so auditing it yields confidently wrong scores.
|
|
189
|
+
if isinstance(spec, dict) and "swagger" in spec:
|
|
190
|
+
print(
|
|
191
|
+
f"error: Swagger {spec['swagger']} is not supported — only OpenAPI 3.x. "
|
|
192
|
+
"Convert it first (e.g. with swagger2openapi).",
|
|
193
|
+
file=sys.stderr,
|
|
194
|
+
)
|
|
195
|
+
return EXIT_SPEC_ERROR
|
|
196
|
+
|
|
150
197
|
if not isinstance(spec, dict) or not spec.get("paths"):
|
|
151
198
|
print(
|
|
152
199
|
"error: no 'paths' found — this does not look like an OpenAPI spec.",
|
|
@@ -154,7 +201,8 @@ def _run(argv=None) -> int:
|
|
|
154
201
|
)
|
|
155
202
|
return EXIT_SPEC_ERROR
|
|
156
203
|
|
|
157
|
-
|
|
204
|
+
# Operations, not paths — the audit (and the ambiguity check) is per operation.
|
|
205
|
+
endpoint_count = len(extract_endpoints(spec))
|
|
158
206
|
result = run_audit(spec, progress=_make_progress(endpoint_count, args.quiet))
|
|
159
207
|
|
|
160
208
|
if result["endpoint_count"] == 0:
|
|
@@ -185,6 +233,37 @@ def _run(argv=None) -> int:
|
|
|
185
233
|
f"({len(ready)}/{result['endpoint_count']} endpoints included)"
|
|
186
234
|
)
|
|
187
235
|
|
|
236
|
+
# Baseline: write, or compare and gate on regressions only.
|
|
237
|
+
if args.write_baseline:
|
|
238
|
+
write_baseline(args.write_baseline, spec, result)
|
|
239
|
+
if not args.quiet:
|
|
240
|
+
print(
|
|
241
|
+
f"Baseline written to {args.write_baseline} "
|
|
242
|
+
f"({len(result['gaps'])} finding(s) recorded as accepted)"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
comparison = None
|
|
246
|
+
if args.baseline:
|
|
247
|
+
try:
|
|
248
|
+
comparison = compare_to_baseline(result, load_baseline(args.baseline))
|
|
249
|
+
except BaselineError as exc:
|
|
250
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
251
|
+
return EXIT_SPEC_ERROR
|
|
252
|
+
|
|
253
|
+
if not args.quiet:
|
|
254
|
+
print()
|
|
255
|
+
print(
|
|
256
|
+
f"Compared against {args.baseline}: "
|
|
257
|
+
f"{comparison['new_count']} new, "
|
|
258
|
+
f"{comparison['fixed_count']} fixed, "
|
|
259
|
+
f"{comparison['unchanged_count']} unchanged"
|
|
260
|
+
)
|
|
261
|
+
for f in comparison["new_findings"][:20]:
|
|
262
|
+
marker = "FAIL" if f.severity == "fail" else "WARN"
|
|
263
|
+
print(f" NEW {marker} {f.endpoint} — {f.message}")
|
|
264
|
+
if len(comparison["new_findings"]) > 20:
|
|
265
|
+
print(f" ... and {len(comparison['new_findings']) - 20} more")
|
|
266
|
+
|
|
188
267
|
# CI gates
|
|
189
268
|
gate_failed = False
|
|
190
269
|
if args.min_score is not None and result["overall_score"] < args.min_score:
|
|
@@ -202,6 +281,18 @@ def _run(argv=None) -> int:
|
|
|
202
281
|
)
|
|
203
282
|
gate_failed = True
|
|
204
283
|
|
|
284
|
+
if (
|
|
285
|
+
comparison is not None
|
|
286
|
+
and args.max_new is not None
|
|
287
|
+
and comparison["new_count"] > args.max_new
|
|
288
|
+
):
|
|
289
|
+
print(
|
|
290
|
+
f"GATE FAILED: {comparison['new_count']} new finding(s) since "
|
|
291
|
+
f"baseline exceeds maximum {args.max_new}",
|
|
292
|
+
file=sys.stderr,
|
|
293
|
+
)
|
|
294
|
+
gate_failed = True
|
|
295
|
+
|
|
205
296
|
return EXIT_GATE_FAILED if gate_failed else EXIT_OK
|
|
206
297
|
|
|
207
298
|
|