agentcheckup 0.4.2__tar.gz → 0.4.4__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.
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/config.yml +2 -2
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/workflows/publish.yml +1 -1
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/CHANGELOG.md +3 -2
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/CONTRIBUTING.md +1 -1
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/LICENSE +1 -1
- agentcheckup-0.4.2/README.md → agentcheckup-0.4.4/PKG-INFO +63 -2
- agentcheckup-0.4.2/PKG-INFO → agentcheckup-0.4.4/README.md +35 -30
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/README.zh-CN.md +27 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/SECURITY.md +1 -1
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/action.yml +1 -1
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/docs/github-action.md +6 -6
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/pyproject.toml +5 -5
- agentcheckup-0.4.4/scripts/benchmark.py +101 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/conflicts.py +28 -3
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/report.py +3 -3
- agentcheckup-0.4.4/tests/fixtures/collision_benchmark.json +177 -0
- agentcheckup-0.4.4/tests/test_benchmark.py +82 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_conflicts.py +37 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.gitattributes +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/rule_request.yml +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/pull_request_template.md +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/workflows/action-selftest.yml +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.github/workflows/ci.yml +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/.gitignore +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/CODE_OF_CONDUCT.md +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/Makefile +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/conftest.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/docs/rules.md +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/scripts/bootstrap.ps1 +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/scripts/bootstrap.sh +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/__init__.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/__main__.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/cli.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/discovery.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/engine.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/loader.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/models.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/__init__.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/base.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/config_rules.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/docs.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/injection.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/naming.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/safety.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/rules/schema.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/src/agentcheckup/tokens.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/bad_tools.json +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/ci_config.json +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/claude_desktop_config.json +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/good_tools.json +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/fixtures/mock_server.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_cli.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_discovery.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_registry.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_rules.py +0 -0
- {agentcheckup-0.4.2 → agentcheckup-0.4.4}/tests/test_scoring.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
blank_issues_enabled: false
|
|
2
2
|
contact_links:
|
|
3
3
|
- name: Question or idea
|
|
4
|
-
url: https://github.com/
|
|
4
|
+
url: https://github.com/jiaqinq/agentcheckup/discussions
|
|
5
5
|
about: For open-ended discussion, use Discussions instead of an issue.
|
|
6
6
|
- name: Security vulnerability
|
|
7
|
-
url: https://github.com/
|
|
7
|
+
url: https://github.com/jiaqinq/agentcheckup/security/advisories/new
|
|
8
8
|
about: Please report security issues privately, not in a public issue.
|
|
@@ -91,8 +91,9 @@ trusts, so the scoring and the report both changed.
|
|
|
91
91
|
### Notes
|
|
92
92
|
|
|
93
93
|
- Conflict detection produced **zero false positives** across the 18 real tools
|
|
94
|
-
from the three official servers.
|
|
95
|
-
|
|
94
|
+
from the three official servers. *(Corrected in 0.4.4: that measurement was
|
|
95
|
+
vacuous — those 18 tools contained no conflicts, so there was nothing to get
|
|
96
|
+
wrong. A real benchmark replaced it.)*
|
|
96
97
|
|
|
97
98
|
## [0.3.0] - 2026-11-07
|
|
98
99
|
|
|
@@ -1,8 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentcheckup
|
|
3
|
+
Version: 0.4.4
|
|
4
|
+
Summary: Diagnose your agent setup: tool conflicts, risky config, context budget.
|
|
5
|
+
Project-URL: Homepage, https://github.com/jiaqinq/agentcheckup
|
|
6
|
+
Project-URL: Issues, https://github.com/jiaqinq/agentcheckup/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/jiaqinq/agentcheckup/blob/main/CHANGELOG.md
|
|
8
|
+
Author: jiaqinq
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,cli,linter,mcp,model-context-protocol,security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
1
29
|
# agentcheckup
|
|
2
30
|
|
|
3
31
|
> Diagnose your agent setup before your agent does.
|
|
4
32
|
|
|
5
|
-
[](https://github.com/jiaqinq/agentcheckup/actions/workflows/ci.yml)
|
|
6
34
|
[](https://pypi.org/project/agentcheckup/)
|
|
7
35
|
[](https://pypi.org/project/agentcheckup/)
|
|
8
36
|
[](LICENSE)
|
|
@@ -121,7 +149,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
|
|
|
121
149
|
the version you run — the action installs from its own checkout.
|
|
122
150
|
|
|
123
151
|
```yaml
|
|
124
|
-
- uses:
|
|
152
|
+
- uses: jiaqinq/agentcheckup@v0.4.4
|
|
125
153
|
with:
|
|
126
154
|
config: .mcp.json
|
|
127
155
|
fail-on: warning
|
|
@@ -165,6 +193,11 @@ Each tool pair across servers is scored on two signals:
|
|
|
165
193
|
- **name overlap** — normalised token Jaccard (`read_files` ≡ `read_file`)
|
|
166
194
|
- **description overlap** — token Jaccard over stopword-stripped, plural-stemmed words
|
|
167
195
|
|
|
196
|
+
Chinese is tokenised as character bigrams (`读取文件` → `读取`, `取文`, `文件`) rather
|
|
197
|
+
than by a segmenter, because a segmenter is a dependency. It is cruder than a real
|
|
198
|
+
word split, but without it every Chinese description produced an empty token set
|
|
199
|
+
and two Chinese tools could never be compared at all.
|
|
200
|
+
|
|
168
201
|
A pair is flagged when the descriptions overlap ≥ 65% **and** the names share a
|
|
169
202
|
head noun, or when the blended score clears 0.55. The shared-noun requirement is
|
|
170
203
|
what keeps `search_repositories` and `search_documents` apart — without it the
|
|
@@ -172,6 +205,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
|
|
|
172
205
|
|
|
173
206
|
It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
|
|
174
207
|
|
|
208
|
+
## How well does conflict detection work?
|
|
209
|
+
|
|
210
|
+
Measured, not asserted. `tests/fixtures/collision_benchmark.json` holds 44
|
|
211
|
+
hand-labelled tool pairs — 20 that should be flagged, 24 that should not — and
|
|
212
|
+
`scripts/benchmark.py` scores the detector against them:
|
|
213
|
+
|
|
214
|
+
| | value |
|
|
215
|
+
|---|---|
|
|
216
|
+
| pairs | 44 (20 positive, 24 negative) |
|
|
217
|
+
| precision | **0.952** |
|
|
218
|
+
| recall | **1.000** |
|
|
219
|
+
| F1 | 0.976 |
|
|
220
|
+
|
|
221
|
+
The one disagreement is a pair like `lookup_contact` / `lookup_user` with
|
|
222
|
+
near-identical descriptions: the detector flags it, the label says it should not.
|
|
223
|
+
That is arguably the detector being right and the label being strict.
|
|
224
|
+
|
|
225
|
+
Two honest caveats. The labels are the author's, not users' — the criterion was
|
|
226
|
+
"would a model picking the wrong one get a materially different result". And this
|
|
227
|
+
benchmark did not exist before 0.4.4; the earlier claim of "zero false positives"
|
|
228
|
+
came from running on 18 tools that contained no conflicts at all, which measured
|
|
229
|
+
nothing. `tests/test_benchmark.py` fails the build if precision or recall drops
|
|
230
|
+
below 0.90.
|
|
231
|
+
|
|
175
232
|
## How the score works
|
|
176
233
|
|
|
177
234
|
Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
|
|
@@ -223,6 +280,10 @@ rule. A report nobody can read is a report nobody acts on.
|
|
|
223
280
|
- Not an MCP protocol validator. It checks quality and safety, not conformance —
|
|
224
281
|
the official `modelcontextprotocol/conformance` suite does that.
|
|
225
282
|
|
|
283
|
+
## Further reading
|
|
284
|
+
|
|
285
|
+
- [多个 MCP Server 一起使用时,如何检测工具冲突?](https://juejin.cn/post/7704995965740580904) — the design story, in Chinese: why the scope changed from a single-server linter to cross-server conflicts, and how the scoring was rebuilt after it graded the official servers 0/100.
|
|
286
|
+
|
|
226
287
|
## Contributing
|
|
227
288
|
|
|
228
289
|
Adding a rule takes about ten minutes and one small file — see
|
|
@@ -1,36 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: agentcheckup
|
|
3
|
-
Version: 0.4.2
|
|
4
|
-
Summary: Diagnose your agent setup: tool conflicts, risky config, context budget.
|
|
5
|
-
Project-URL: Homepage, https://github.com/wanjiaqing/agentcheckup
|
|
6
|
-
Project-URL: Issues, https://github.com/wanjiaqing/agentcheckup/issues
|
|
7
|
-
Project-URL: Changelog, https://github.com/wanjiaqing/agentcheckup/blob/main/CHANGELOG.md
|
|
8
|
-
Author: wanjiaqing
|
|
9
|
-
License: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: agents,ai,cli,linter,mcp,model-context-protocol,security
|
|
12
|
-
Classifier: Development Status :: 3 - Alpha
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
-
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
-
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: >=3.10
|
|
24
|
-
Provides-Extra: dev
|
|
25
|
-
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
-
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
-
Description-Content-Type: text/markdown
|
|
28
|
-
|
|
29
1
|
# agentcheckup
|
|
30
2
|
|
|
31
3
|
> Diagnose your agent setup before your agent does.
|
|
32
4
|
|
|
33
|
-
[](https://github.com/jiaqinq/agentcheckup/actions/workflows/ci.yml)
|
|
34
6
|
[](https://pypi.org/project/agentcheckup/)
|
|
35
7
|
[](https://pypi.org/project/agentcheckup/)
|
|
36
8
|
[](LICENSE)
|
|
@@ -149,7 +121,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
|
|
|
149
121
|
the version you run — the action installs from its own checkout.
|
|
150
122
|
|
|
151
123
|
```yaml
|
|
152
|
-
- uses:
|
|
124
|
+
- uses: jiaqinq/agentcheckup@v0.4.4
|
|
153
125
|
with:
|
|
154
126
|
config: .mcp.json
|
|
155
127
|
fail-on: warning
|
|
@@ -193,6 +165,11 @@ Each tool pair across servers is scored on two signals:
|
|
|
193
165
|
- **name overlap** — normalised token Jaccard (`read_files` ≡ `read_file`)
|
|
194
166
|
- **description overlap** — token Jaccard over stopword-stripped, plural-stemmed words
|
|
195
167
|
|
|
168
|
+
Chinese is tokenised as character bigrams (`读取文件` → `读取`, `取文`, `文件`) rather
|
|
169
|
+
than by a segmenter, because a segmenter is a dependency. It is cruder than a real
|
|
170
|
+
word split, but without it every Chinese description produced an empty token set
|
|
171
|
+
and two Chinese tools could never be compared at all.
|
|
172
|
+
|
|
196
173
|
A pair is flagged when the descriptions overlap ≥ 65% **and** the names share a
|
|
197
174
|
head noun, or when the blended score clears 0.55. The shared-noun requirement is
|
|
198
175
|
what keeps `search_repositories` and `search_documents` apart — without it the
|
|
@@ -200,6 +177,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
|
|
|
200
177
|
|
|
201
178
|
It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
|
|
202
179
|
|
|
180
|
+
## How well does conflict detection work?
|
|
181
|
+
|
|
182
|
+
Measured, not asserted. `tests/fixtures/collision_benchmark.json` holds 44
|
|
183
|
+
hand-labelled tool pairs — 20 that should be flagged, 24 that should not — and
|
|
184
|
+
`scripts/benchmark.py` scores the detector against them:
|
|
185
|
+
|
|
186
|
+
| | value |
|
|
187
|
+
|---|---|
|
|
188
|
+
| pairs | 44 (20 positive, 24 negative) |
|
|
189
|
+
| precision | **0.952** |
|
|
190
|
+
| recall | **1.000** |
|
|
191
|
+
| F1 | 0.976 |
|
|
192
|
+
|
|
193
|
+
The one disagreement is a pair like `lookup_contact` / `lookup_user` with
|
|
194
|
+
near-identical descriptions: the detector flags it, the label says it should not.
|
|
195
|
+
That is arguably the detector being right and the label being strict.
|
|
196
|
+
|
|
197
|
+
Two honest caveats. The labels are the author's, not users' — the criterion was
|
|
198
|
+
"would a model picking the wrong one get a materially different result". And this
|
|
199
|
+
benchmark did not exist before 0.4.4; the earlier claim of "zero false positives"
|
|
200
|
+
came from running on 18 tools that contained no conflicts at all, which measured
|
|
201
|
+
nothing. `tests/test_benchmark.py` fails the build if precision or recall drops
|
|
202
|
+
below 0.90.
|
|
203
|
+
|
|
203
204
|
## How the score works
|
|
204
205
|
|
|
205
206
|
Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
|
|
@@ -251,6 +252,10 @@ rule. A report nobody can read is a report nobody acts on.
|
|
|
251
252
|
- Not an MCP protocol validator. It checks quality and safety, not conformance —
|
|
252
253
|
the official `modelcontextprotocol/conformance` suite does that.
|
|
253
254
|
|
|
255
|
+
## Further reading
|
|
256
|
+
|
|
257
|
+
- [多个 MCP Server 一起使用时,如何检测工具冲突?](https://juejin.cn/post/7704995965740580904) — the design story, in Chinese: why the scope changed from a single-server linter to cross-server conflicts, and how the scoring was rebuilt after it graded the official servers 0/100.
|
|
258
|
+
|
|
254
259
|
## Contributing
|
|
255
260
|
|
|
256
261
|
Adding a rule takes about ten minutes and one small file — see
|
|
@@ -118,12 +118,35 @@ agentcheckup rules # 列出全部规则
|
|
|
118
118
|
- **名字重叠**:归一化后的 token Jaccard(`read_files` ≡ `read_file`)
|
|
119
119
|
- **描述重叠**:去停用词、粗词干化之后的 token Jaccard
|
|
120
120
|
|
|
121
|
+
中文按**字符二元组**切分(`读取文件` → `读取`、`取文`、`文件`),没有引入分词器——
|
|
122
|
+
分词器就是依赖。这比真正的分词粗,但没有它的时候中文描述会得到空集合,
|
|
123
|
+
两个中文工具根本无法比较。
|
|
124
|
+
|
|
121
125
|
判定条件:描述重叠 ≥ 65% **且** 名字共享中心词,或加权分 ≥ 0.55。
|
|
122
126
|
共享中心词这个约束是关键——没有它,`search_repositories` 和 `search_documents`
|
|
123
127
|
会因为都含 "search" 被误报,而**充满噪音的报告比没有报告更糟**。
|
|
124
128
|
|
|
125
129
|
这是启发式,文档里写明了。`AGT203` 是警告,不是判决。
|
|
126
130
|
|
|
131
|
+
## 冲突检测到底准不准
|
|
132
|
+
|
|
133
|
+
这是测出来的,不是声称的。`tests/fixtures/collision_benchmark.json` 里有 44 对人工标注的工具对
|
|
134
|
+
(20 对该报、24 对不该报),`scripts/benchmark.py` 拿检测器去跑:
|
|
135
|
+
|
|
136
|
+
| 指标 | 值 |
|
|
137
|
+
|---|---|
|
|
138
|
+
| 样本 | 44 对(20 正 / 24 负) |
|
|
139
|
+
| 精确率 | **0.952** |
|
|
140
|
+
| 召回率 | **1.000** |
|
|
141
|
+
| F1 | 0.976 |
|
|
142
|
+
|
|
143
|
+
唯一的分歧是一对类似 `lookup_contact` / `lookup_user`、描述几乎一样的工具——检测器报了,
|
|
144
|
+
我的标注说不该报。**这更像是检测器对、标注太严。**
|
|
145
|
+
|
|
146
|
+
两个必须说明的限制:标注是我自己做的,不是用户标注(判断标准是「模型选错会不会拿到实质不同的结果」);
|
|
147
|
+
这个基准是 0.4.4 才有的,**之前那句「零误报」是在 18 个根本没有冲突的工具上跑出来的,什么都没测到**。
|
|
148
|
+
`tests/test_benchmark.py` 会在精确率或召回率跌破 0.90 时让 CI 失败。
|
|
149
|
+
|
|
127
150
|
## 分数是怎么算的
|
|
128
151
|
|
|
129
152
|
大多数 linter 在这里翻车:一条在 18 个工具上全部触发的规则,是**偏好**不是缺陷,
|
|
@@ -165,6 +188,10 @@ agentcheckup rules # 列出全部规则
|
|
|
165
188
|
- 不是运行时防护。它不在 agent 会话里运行。
|
|
166
189
|
- 不是 MCP 协议校验器。协议一致性由官方 `modelcontextprotocol/conformance` 负责。
|
|
167
190
|
|
|
191
|
+
## 相关文章
|
|
192
|
+
|
|
193
|
+
- [多个 MCP Server 一起使用时,如何检测工具冲突?](https://juejin.cn/post/7704995965740580904) —— 设计复盘:为什么从单 server linter 改成跨 server 冲突检测,以及评分把官方 server 打成 0 分之后怎么重做。
|
|
194
|
+
|
|
168
195
|
## 贡献
|
|
169
196
|
|
|
170
197
|
加一条规则大约十分钟、一个文件,见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
@@ -14,7 +14,7 @@ Nothing leaves your machine.
|
|
|
14
14
|
## Reporting a vulnerability
|
|
15
15
|
|
|
16
16
|
Use GitHub's private advisory flow:
|
|
17
|
-
`https://github.com/
|
|
17
|
+
`https://github.com/jiaqinq/agentcheckup/security/advisories/new`
|
|
18
18
|
|
|
19
19
|
Please do not open a public issue for a vulnerability. Expect an initial response
|
|
20
20
|
within 7 days.
|
|
@@ -13,7 +13,7 @@ jobs:
|
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
steps:
|
|
15
15
|
- uses: actions/checkout@v4
|
|
16
|
-
- uses:
|
|
16
|
+
- uses: jiaqinq/agentcheckup@main
|
|
17
17
|
with:
|
|
18
18
|
config: .mcp.json
|
|
19
19
|
fail-on: warning
|
|
@@ -45,7 +45,7 @@ Outputs are published even when the gate fails, so you can gate on score without
|
|
|
45
45
|
parsing the log:
|
|
46
46
|
|
|
47
47
|
```yaml
|
|
48
|
-
- uses:
|
|
48
|
+
- uses: jiaqinq/agentcheckup@main
|
|
49
49
|
id: doctor
|
|
50
50
|
with:
|
|
51
51
|
config: .mcp.json
|
|
@@ -64,7 +64,7 @@ permissions:
|
|
|
64
64
|
|
|
65
65
|
steps:
|
|
66
66
|
- uses: actions/checkout@v4
|
|
67
|
-
- uses:
|
|
67
|
+
- uses: jiaqinq/agentcheckup@main
|
|
68
68
|
with:
|
|
69
69
|
config: .mcp.json
|
|
70
70
|
sarif-file: agentcheckup.sarif
|
|
@@ -87,7 +87,7 @@ for locally. On a fresh runner that usually finds nothing, and agentcheckup exit
|
|
|
87
87
|
For CI, pass the config explicitly:
|
|
88
88
|
|
|
89
89
|
```yaml
|
|
90
|
-
- uses:
|
|
90
|
+
- uses: jiaqinq/agentcheckup@main
|
|
91
91
|
with:
|
|
92
92
|
config: |
|
|
93
93
|
.mcp.json
|
|
@@ -99,7 +99,7 @@ For CI, pass the config explicitly:
|
|
|
99
99
|
Spawning every MCP server takes time. If you only need the config checks:
|
|
100
100
|
|
|
101
101
|
```yaml
|
|
102
|
-
- uses:
|
|
102
|
+
- uses: jiaqinq/agentcheckup@main
|
|
103
103
|
with:
|
|
104
104
|
config: .mcp.json
|
|
105
105
|
args: --no-spawn
|
|
@@ -113,5 +113,5 @@ are skipped too. It is fast and useful, but it is not the full check.
|
|
|
113
113
|
`@main` tracks the branch. For reproducible CI, pin a tag or a commit SHA:
|
|
114
114
|
|
|
115
115
|
```yaml
|
|
116
|
-
- uses:
|
|
116
|
+
- uses: jiaqinq/agentcheckup@v0.3.0
|
|
117
117
|
```
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentcheckup"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.4"
|
|
8
8
|
description = "Diagnose your agent setup: tool conflicts, risky config, context budget."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
|
-
authors = [{ name = "
|
|
12
|
+
authors = [{ name = "jiaqinq" }]
|
|
13
13
|
keywords = ["mcp", "model-context-protocol", "agents", "ai", "linter", "security", "cli"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 3 - Alpha",
|
|
@@ -33,9 +33,9 @@ dev = ["pytest>=8", "ruff>=0.6"]
|
|
|
33
33
|
agentcheckup = "agentcheckup.cli:main"
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
36
|
-
Homepage = "https://github.com/
|
|
37
|
-
Issues = "https://github.com/
|
|
38
|
-
Changelog = "https://github.com/
|
|
36
|
+
Homepage = "https://github.com/jiaqinq/agentcheckup"
|
|
37
|
+
Issues = "https://github.com/jiaqinq/agentcheckup/issues"
|
|
38
|
+
Changelog = "https://github.com/jiaqinq/agentcheckup/blob/main/CHANGELOG.md"
|
|
39
39
|
|
|
40
40
|
[tool.hatch.build.targets.wheel]
|
|
41
41
|
packages = ["src/agentcheckup"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Measure conflict detection against a labelled set of tool pairs.
|
|
2
|
+
|
|
3
|
+
The README used to claim "zero false positives" on the strength of three official
|
|
4
|
+
servers that contained no conflicts at all. That is not a measurement. This is:
|
|
5
|
+
every pair below was labelled by hand, so precision and recall can be computed
|
|
6
|
+
and regressions become visible.
|
|
7
|
+
|
|
8
|
+
python scripts/benchmark.py
|
|
9
|
+
|
|
10
|
+
Labels are the author's, not users'. The criterion applied to each pair:
|
|
11
|
+
"would a maintainer reasonably consider these two tools confusable by a model
|
|
12
|
+
choosing from one flat list?" A pair is positive when a user picking the wrong
|
|
13
|
+
one would get a materially different result.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
24
|
+
|
|
25
|
+
from agentcheckup.conflicts import ToolRef, detect_conflicts # noqa: E402
|
|
26
|
+
from agentcheckup.models import Tool # noqa: E402
|
|
27
|
+
|
|
28
|
+
FIXTURE = ROOT / "tests" / "fixtures" / "collision_benchmark.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_ref(side: dict) -> ToolRef:
|
|
32
|
+
tool = Tool.from_raw(
|
|
33
|
+
{
|
|
34
|
+
"name": side["name"],
|
|
35
|
+
"description": side["description"],
|
|
36
|
+
"inputSchema": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {"path": {"type": "string", "pattern": "^.*$"}},
|
|
39
|
+
"additionalProperties": False,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
return ToolRef(server=side["server"], tool=tool)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> int:
|
|
47
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
48
|
+
|
|
49
|
+
tp = fp = tn = fn = 0
|
|
50
|
+
false_positives: list[str] = []
|
|
51
|
+
false_negatives: list[str] = []
|
|
52
|
+
|
|
53
|
+
for case in cases:
|
|
54
|
+
found = detect_conflicts([to_ref(case["a"]), to_ref(case["b"])])
|
|
55
|
+
predicted = bool(found)
|
|
56
|
+
actual = case["conflict"]
|
|
57
|
+
|
|
58
|
+
if predicted and actual:
|
|
59
|
+
tp += 1
|
|
60
|
+
elif predicted and not actual:
|
|
61
|
+
fp += 1
|
|
62
|
+
rules = ",".join(sorted({c.rule for c in found}))
|
|
63
|
+
false_positives.append(f" {case['id']} {rules} {case['why']}")
|
|
64
|
+
elif not predicted and actual:
|
|
65
|
+
fn += 1
|
|
66
|
+
false_negatives.append(f" {case['id']} missed {case['why']}")
|
|
67
|
+
else:
|
|
68
|
+
tn += 1
|
|
69
|
+
|
|
70
|
+
total = tp + fp + tn + fn
|
|
71
|
+
precision = tp / (tp + fp) if tp + fp else 0.0
|
|
72
|
+
recall = tp / (tp + fn) if tp + fn else 0.0
|
|
73
|
+
f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
|
|
74
|
+
accuracy = (tp + tn) / total if total else 0.0
|
|
75
|
+
|
|
76
|
+
print(f"pairs {total}")
|
|
77
|
+
print(f"positives {tp + fn}")
|
|
78
|
+
print(f"negatives {tn + fp}")
|
|
79
|
+
print()
|
|
80
|
+
print(f"true pos {tp}")
|
|
81
|
+
print(f"false pos {fp}")
|
|
82
|
+
print(f"true neg {tn}")
|
|
83
|
+
print(f"false neg {fn}")
|
|
84
|
+
print()
|
|
85
|
+
print(f"precision {precision:.3f}")
|
|
86
|
+
print(f"recall {recall:.3f}")
|
|
87
|
+
print(f"f1 {f1:.3f}")
|
|
88
|
+
print(f"accuracy {accuracy:.3f}")
|
|
89
|
+
|
|
90
|
+
if false_positives:
|
|
91
|
+
print("\nfalse positives")
|
|
92
|
+
print("\n".join(false_positives))
|
|
93
|
+
if false_negatives:
|
|
94
|
+
print("\nfalse negatives")
|
|
95
|
+
print("\n".join(false_negatives))
|
|
96
|
+
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
raise SystemExit(main())
|
|
@@ -42,6 +42,10 @@ STOPWORDS = frozenset(
|
|
|
42
42
|
|
|
43
43
|
WORD = re.compile(r"[a-z0-9]+")
|
|
44
44
|
|
|
45
|
+
#: CJK ideographs, extension A included. Matched per character because Chinese
|
|
46
|
+
#: has no word delimiters and a real segmenter would be a dependency.
|
|
47
|
+
CJK = re.compile(r"[\u3400-\u4dbf\u4e00-\u9fff]+")
|
|
48
|
+
|
|
45
49
|
|
|
46
50
|
@dataclass(frozen=True)
|
|
47
51
|
class ToolRef:
|
|
@@ -94,13 +98,33 @@ def _stem(word: str) -> str:
|
|
|
94
98
|
return word
|
|
95
99
|
|
|
96
100
|
|
|
101
|
+
def _cjk_tokens(text: str) -> set[str]:
|
|
102
|
+
"""Character bigrams over every CJK run.
|
|
103
|
+
|
|
104
|
+
Chinese has no spaces, so ``WORD`` returns nothing and two Chinese tools
|
|
105
|
+
would look like two empty sets — silently, which is the worst outcome. A real
|
|
106
|
+
segmenter is a dependency; character bigrams are the standard cheap
|
|
107
|
+
substitute: 读取文件 -> 读取, 取文, 文件.
|
|
108
|
+
|
|
109
|
+
A single-character run is kept whole, so 读 still matches 读.
|
|
110
|
+
"""
|
|
111
|
+
out: set[str] = set()
|
|
112
|
+
for run in CJK.findall(text):
|
|
113
|
+
if len(run) == 1:
|
|
114
|
+
out.add(run)
|
|
115
|
+
else:
|
|
116
|
+
out.update(run[i : i + 2] for i in range(len(run) - 1))
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
|
|
97
120
|
def tokens(text: str) -> set[str]:
|
|
98
|
-
"""Lowercase word set, stopwords removed, crude plural stemming."""
|
|
99
|
-
|
|
121
|
+
"""Lowercase word set, stopwords removed, crude plural stemming, plus CJK."""
|
|
122
|
+
latin = {
|
|
100
123
|
_stem(w)
|
|
101
124
|
for w in WORD.findall(text.lower())
|
|
102
125
|
if w not in STOPWORDS and len(w) > 1
|
|
103
126
|
}
|
|
127
|
+
return latin | _cjk_tokens(text)
|
|
104
128
|
|
|
105
129
|
|
|
106
130
|
def jaccard(a: set[str], b: set[str]) -> float:
|
|
@@ -111,7 +135,8 @@ def jaccard(a: set[str], b: set[str]) -> float:
|
|
|
111
135
|
|
|
112
136
|
|
|
113
137
|
def name_tokens(name: str) -> set[str]:
|
|
114
|
-
|
|
138
|
+
latin = {_stem(w) for w in WORD.findall(name.lower()) if len(w) > 1}
|
|
139
|
+
return latin | _cjk_tokens(name)
|
|
115
140
|
|
|
116
141
|
|
|
117
142
|
def similarity(a: ToolRef, b: ToolRef) -> float:
|
|
@@ -10,7 +10,7 @@ from . import rules as _rules
|
|
|
10
10
|
from .engine import Report
|
|
11
11
|
from .models import Finding, Severity
|
|
12
12
|
|
|
13
|
-
__version__ = "0.4.
|
|
13
|
+
__version__ = "0.4.4"
|
|
14
14
|
|
|
15
15
|
SARIF_LEVEL = {
|
|
16
16
|
Severity.ERROR: "error",
|
|
@@ -195,7 +195,7 @@ def render_text(report: Report, verbose: bool = False) -> str:
|
|
|
195
195
|
lines.extend(_nudge_lines(nudges))
|
|
196
196
|
|
|
197
197
|
lines.append("")
|
|
198
|
-
lines.append(" Rule docs: https://github.com/
|
|
198
|
+
lines.append(" Rule docs: https://github.com/jiaqinq/agentcheckup#rules")
|
|
199
199
|
return "\n".join(lines)
|
|
200
200
|
|
|
201
201
|
|
|
@@ -246,7 +246,7 @@ def render_sarif(report: Report) -> str:
|
|
|
246
246
|
"driver": {
|
|
247
247
|
"name": "agentcheckup",
|
|
248
248
|
"version": __version__,
|
|
249
|
-
"informationUri": "https://github.com/
|
|
249
|
+
"informationUri": "https://github.com/jiaqinq/agentcheckup",
|
|
250
250
|
"rules": _rule_metadata(),
|
|
251
251
|
}
|
|
252
252
|
},
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"id": "p01", "conflict": true, "why": "同名工具,跨 server",
|
|
3
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a text file from the sandbox and return its contents."},
|
|
4
|
+
"b": {"server": "git", "name": "read_file", "description": "Read a file as it exists in a git revision."}},
|
|
5
|
+
|
|
6
|
+
{"id": "p02", "conflict": true, "why": "同名,仅大小写不同,检测器会先 lower()",
|
|
7
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a file from disk."},
|
|
8
|
+
"b": {"server": "s3", "name": "Read_File", "description": "Read an object from a bucket."}},
|
|
9
|
+
|
|
10
|
+
{"id": "p03", "conflict": true, "why": "单复数差异,归一化后同名",
|
|
11
|
+
"a": {"server": "files", "name": "read_files", "description": "Read several files and return their contents."},
|
|
12
|
+
"b": {"server": "git", "name": "read_file", "description": "Read one file from a revision."}},
|
|
13
|
+
|
|
14
|
+
{"id": "p04", "conflict": true, "why": "描述几乎一致,名字共享 search",
|
|
15
|
+
"a": {"server": "github", "name": "search_public", "description": "Search public repositories by keyword and return matching repository names."},
|
|
16
|
+
"b": {"server": "docs", "name": "search_private", "description": "Search private repositories by keyword and return matching repository names."}},
|
|
17
|
+
|
|
18
|
+
{"id": "p05", "conflict": true, "why": "同义动词 + 同一宾语,名字共享 file",
|
|
19
|
+
"a": {"server": "files", "name": "get_file", "description": "Fetch a file by path and return its bytes."},
|
|
20
|
+
"b": {"server": "docs", "name": "get_files", "description": "Fetch files by path and return their bytes."}},
|
|
21
|
+
|
|
22
|
+
{"id": "p06", "conflict": true, "why": "中文近义,描述几乎相同",
|
|
23
|
+
"a": {"server": "文件服务", "name": "读取文件", "description": "读取指定路径的文件内容并返回文本"},
|
|
24
|
+
"b": {"server": "文档服务", "name": "读取文档", "description": "读取指定路径的文件内容并返回字符串"}},
|
|
25
|
+
|
|
26
|
+
{"id": "p07", "conflict": true, "why": "同名删除工具",
|
|
27
|
+
"a": {"server": "files", "name": "delete_file", "description": "Delete a file at the given path."},
|
|
28
|
+
"b": {"server": "storage", "name": "delete_file", "description": "Delete an object from the bucket."}},
|
|
29
|
+
|
|
30
|
+
{"id": "p08", "conflict": true, "why": "单复数 + 同一目录语义",
|
|
31
|
+
"a": {"server": "files", "name": "list_dir", "description": "List entries in one directory under the sandbox root."},
|
|
32
|
+
"b": {"server": "shell", "name": "list_dirs", "description": "List entries in one directory under the sandbox root."}},
|
|
33
|
+
|
|
34
|
+
{"id": "p09", "conflict": true, "why": "同一动作的 issue 创建,描述高度重叠",
|
|
35
|
+
"a": {"server": "github", "name": "create_issue", "description": "Create an issue with a title, body and assignee in a repository."},
|
|
36
|
+
"b": {"server": "jira", "name": "create_issues", "description": "Create an issue with a title, body and assignee in a project."}},
|
|
37
|
+
|
|
38
|
+
{"id": "p10", "conflict": true, "why": "描述相同,名字只差一个词且共享 run",
|
|
39
|
+
"a": {"server": "shell", "name": "run_command", "description": "Run a shell command and return stdout and stderr."},
|
|
40
|
+
"b": {"server": "docker", "name": "run_commands", "description": "Run a shell command and return stdout and stderr."}},
|
|
41
|
+
|
|
42
|
+
{"id": "p11", "conflict": true, "why": "同名,一个本地一个远端",
|
|
43
|
+
"a": {"server": "files", "name": "write_file", "description": "Write text to a file on disk."},
|
|
44
|
+
"b": {"server": "s3", "name": "write_file", "description": "Write text to an object in a bucket."}},
|
|
45
|
+
|
|
46
|
+
{"id": "p12", "conflict": true, "why": "描述几乎逐字相同",
|
|
47
|
+
"a": {"server": "db", "name": "query_table", "description": "Query rows from a table using a SQL where clause and return them as JSON."},
|
|
48
|
+
"b": {"server": "warehouse", "name": "query_tables", "description": "Query rows from a table using a SQL where clause and return them as JSON."}},
|
|
49
|
+
|
|
50
|
+
{"id": "p13", "conflict": true, "why": "同一份数据的两种读取入口,名字共享 read",
|
|
51
|
+
"a": {"server": "cache", "name": "read_cache", "description": "Read a value from the cache by key."},
|
|
52
|
+
"b": {"server": "redis", "name": "read_cached", "description": "Read a value from the cache by key."}},
|
|
53
|
+
|
|
54
|
+
{"id": "p14", "conflict": true, "why": "中文同名",
|
|
55
|
+
"a": {"server": "文件服务", "name": "搜索文件", "description": "按关键词搜索文件并返回匹配的路径"},
|
|
56
|
+
"b": {"server": "代码服务", "name": "搜索文件", "description": "按关键词搜索仓库中的文件"}},
|
|
57
|
+
|
|
58
|
+
{"id": "p15", "conflict": true, "why": "单复数 + 同一搜索动作",
|
|
59
|
+
"a": {"server": "docs", "name": "search_doc", "description": "Search documents by title across the workspace and return matching ids."},
|
|
60
|
+
"b": {"server": "wiki", "name": "search_docs", "description": "Search documents by title across the workspace and return matching ids."}},
|
|
61
|
+
|
|
62
|
+
{"id": "p16", "conflict": true, "why": "同名上传",
|
|
63
|
+
"a": {"server": "s3", "name": "upload", "description": "Upload a local file to remote storage."},
|
|
64
|
+
"b": {"server": "gcs", "name": "upload", "description": "Upload a local file to remote storage."}},
|
|
65
|
+
|
|
66
|
+
{"id": "p17", "conflict": true, "why": "描述一致,名字共享 get 与 issue",
|
|
67
|
+
"a": {"server": "github", "name": "get_issue", "description": "Get one issue by number and return its title, body, state and labels."},
|
|
68
|
+
"b": {"server": "jira", "name": "get_issues", "description": "Get one issue by number and return its title, body, state and labels."}},
|
|
69
|
+
|
|
70
|
+
{"id": "p18", "conflict": true, "why": "同一动作的中英文混写,描述相同",
|
|
71
|
+
"a": {"server": "files", "name": "read_file", "description": "读取文件内容并返回文本"},
|
|
72
|
+
"b": {"server": "docs", "name": "read_file", "description": "读取文件内容并返回文本"}},
|
|
73
|
+
|
|
74
|
+
{"id": "p19", "conflict": true, "why": "单复数 + 同一删除动作,描述相同",
|
|
75
|
+
"a": {"server": "db", "name": "drop_table", "description": "Drop a table and all of its rows."},
|
|
76
|
+
"b": {"server": "warehouse", "name": "drop_tables", "description": "Drop a table and all of its rows."}},
|
|
77
|
+
|
|
78
|
+
{"id": "p20", "conflict": true, "why": "同名创建",
|
|
79
|
+
"a": {"server": "k8s", "name": "create_namespace", "description": "Create a namespace in the cluster."},
|
|
80
|
+
"b": {"server": "cloud", "name": "create_namespace", "description": "Create a namespace in the project."}},
|
|
81
|
+
|
|
82
|
+
{"id": "n01", "conflict": false, "why": "完全不同的事",
|
|
83
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a text file from the sandbox and return its contents."},
|
|
84
|
+
"b": {"server": "jira", "name": "create_ticket", "description": "Create a ticket with a title, body and assignee."}},
|
|
85
|
+
|
|
86
|
+
{"id": "n02", "conflict": false, "why": "共享 search 但宾语不同,这是最常见的误报来源",
|
|
87
|
+
"a": {"server": "github", "name": "search_repositories", "description": "Search repositories by name across the org."},
|
|
88
|
+
"b": {"server": "docs", "name": "search_documents", "description": "Search documents by title across the workspace."}},
|
|
89
|
+
|
|
90
|
+
{"id": "n03", "conflict": false, "why": "共享 get 但对象完全不同",
|
|
91
|
+
"a": {"server": "time", "name": "get_current_time", "description": "Return the current time in a given timezone."},
|
|
92
|
+
"b": {"server": "weather", "name": "get_weather", "description": "Return the forecast for a city and date."}},
|
|
93
|
+
|
|
94
|
+
{"id": "n04", "conflict": false, "why": "共享 list 但宾语不同",
|
|
95
|
+
"a": {"server": "files", "name": "list_files", "description": "List files under a directory."},
|
|
96
|
+
"b": {"server": "db", "name": "list_tables", "description": "List tables in a database."}},
|
|
97
|
+
|
|
98
|
+
{"id": "n05", "conflict": false, "why": "共享 upload 但媒体类型不同",
|
|
99
|
+
"a": {"server": "assets", "name": "upload_image", "description": "Upload an image and return its asset id."},
|
|
100
|
+
"b": {"server": "docs", "name": "upload_document", "description": "Upload a document and return its document id."}},
|
|
101
|
+
|
|
102
|
+
{"id": "n06", "conflict": false, "why": "中文共享文件但动作相反",
|
|
103
|
+
"a": {"server": "文件服务", "name": "读取文件", "description": "读取指定路径的文件内容并返回文本"},
|
|
104
|
+
"b": {"server": "文件服务", "name": "删除文件", "description": "删除指定路径的文件"}},
|
|
105
|
+
|
|
106
|
+
{"id": "n07", "conflict": false, "why": "共享 file 但读与写相反",
|
|
107
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a file and return its contents."},
|
|
108
|
+
"b": {"server": "files", "name": "write_file", "description": "Write text to a file."}},
|
|
109
|
+
|
|
110
|
+
{"id": "n08", "conflict": false, "why": "共享 git 但动作不同",
|
|
111
|
+
"a": {"server": "git", "name": "git_commit", "description": "Record staged changes as a commit."},
|
|
112
|
+
"b": {"server": "git", "name": "git_status", "description": "Show the working tree status."}},
|
|
113
|
+
|
|
114
|
+
{"id": "n09", "conflict": false, "why": "名字完全不共享词",
|
|
115
|
+
"a": {"server": "fetch", "name": "fetch", "description": "Fetch a URL and convert it to markdown."},
|
|
116
|
+
"b": {"server": "time", "name": "convert_time", "description": "Convert a time between timezones."}},
|
|
117
|
+
|
|
118
|
+
{"id": "n10", "conflict": false, "why": "共享 create 但资源不同",
|
|
119
|
+
"a": {"server": "github", "name": "create_branch", "description": "Create a git branch in a repository."},
|
|
120
|
+
"b": {"server": "jira", "name": "create_project", "description": "Create a project with a key and name."}},
|
|
121
|
+
|
|
122
|
+
{"id": "n11", "conflict": false, "why": "名字共享 file,但一个是读内容一个是读元数据",
|
|
123
|
+
"a": {"server": "files", "name": "read_file", "description": "Read the full text content of a file from disk."},
|
|
124
|
+
"b": {"server": "meta", "name": "file_info", "description": "Return size, mtime and permissions for a file."}},
|
|
125
|
+
|
|
126
|
+
{"id": "n12", "conflict": false, "why": "共享 search 但一个是代码一个是日志",
|
|
127
|
+
"a": {"server": "code", "name": "search_code", "description": "Search source files for a pattern."},
|
|
128
|
+
"b": {"server": "logs", "name": "search_logs", "description": "Search application logs for a pattern."}},
|
|
129
|
+
|
|
130
|
+
{"id": "n13", "conflict": false, "why": "共享 send 但渠道不同",
|
|
131
|
+
"a": {"server": "slack", "name": "send_message", "description": "Send a message to a channel."},
|
|
132
|
+
"b": {"server": "mail", "name": "send_email", "description": "Send an email to a recipient."}},
|
|
133
|
+
|
|
134
|
+
{"id": "n14", "conflict": false, "why": "中文共享仓库但动作不同",
|
|
135
|
+
"a": {"server": "代码服务", "name": "克隆仓库", "description": "把远端仓库克隆到本地目录"},
|
|
136
|
+
"b": {"server": "代码服务", "name": "删除仓库", "description": "删除远端仓库及其全部分支"}},
|
|
137
|
+
|
|
138
|
+
{"id": "n15", "conflict": false, "why": "共享 run 但一个是测试一个是迁移",
|
|
139
|
+
"a": {"server": "ci", "name": "run_tests", "description": "Run the test suite and return failures."},
|
|
140
|
+
"b": {"server": "db", "name": "run_migrations", "description": "Apply pending database migrations."}},
|
|
141
|
+
|
|
142
|
+
{"id": "n16", "conflict": false, "why": "共享 get 但一个是配置一个是统计",
|
|
143
|
+
"a": {"server": "config", "name": "get_config", "description": "Return the current configuration as JSON."},
|
|
144
|
+
"b": {"server": "metrics", "name": "get_stats", "description": "Return request counts for the last hour."}},
|
|
145
|
+
|
|
146
|
+
{"id": "n17", "conflict": false, "why": "描述用词相似但对象不同(跨域同义)",
|
|
147
|
+
"a": {"server": "crm", "name": "lookup_contact", "description": "Look up a contact by email and return the profile."},
|
|
148
|
+
"b": {"server": "auth", "name": "lookup_user", "description": "Look up a user by email and return the profile."}},
|
|
149
|
+
|
|
150
|
+
{"id": "n18", "conflict": false, "why": "共享 list 但一个是文件一个是容器",
|
|
151
|
+
"a": {"server": "docker", "name": "list_containers", "description": "List running containers."},
|
|
152
|
+
"b": {"server": "files", "name": "list_directory", "description": "List entries in a directory."}},
|
|
153
|
+
|
|
154
|
+
{"id": "n19", "conflict": false, "why": "共享 delete 但资源不同",
|
|
155
|
+
"a": {"server": "k8s", "name": "delete_pod", "description": "Delete a pod by name."},
|
|
156
|
+
"b": {"server": "s3", "name": "delete_object", "description": "Delete an object by key."}},
|
|
157
|
+
|
|
158
|
+
{"id": "n20", "conflict": false, "why": "名字共享 repo,一个是读一个是搜",
|
|
159
|
+
"a": {"server": "github", "name": "get_repo", "description": "Return metadata for one repository."},
|
|
160
|
+
"b": {"server": "github", "name": "search_repos", "description": "Search repositories by keyword."}},
|
|
161
|
+
|
|
162
|
+
{"id": "n21", "conflict": false, "why": "共享 write 但一个是文件一个是日志",
|
|
163
|
+
"a": {"server": "files", "name": "write_file", "description": "Write text to a file on disk."},
|
|
164
|
+
"b": {"server": "logs", "name": "write_log", "description": "Append a line to the application log."}},
|
|
165
|
+
|
|
166
|
+
{"id": "n22", "conflict": false, "why": "共享 create 但一个是目录一个是分支",
|
|
167
|
+
"a": {"server": "files", "name": "create_dir", "description": "Create a directory under the sandbox root."},
|
|
168
|
+
"b": {"server": "git", "name": "create_branch", "description": "Create a branch at the current HEAD."}},
|
|
169
|
+
|
|
170
|
+
{"id": "n23", "conflict": false, "why": "中文共享搜索但对象不同",
|
|
171
|
+
"a": {"server": "代码服务", "name": "搜索代码", "description": "在源码文件中搜索指定模式"},
|
|
172
|
+
"b": {"server": "日志服务", "name": "搜索日志", "description": "在应用日志中搜索指定模式"}},
|
|
173
|
+
|
|
174
|
+
{"id": "n24", "conflict": false, "why": "共享 read 但一个是文件一个是环境变量",
|
|
175
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a file and return its contents."},
|
|
176
|
+
"b": {"server": "env", "name": "read_env", "description": "Read an environment variable by name."}}
|
|
177
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Regression guard on the labelled collision benchmark.
|
|
2
|
+
|
|
3
|
+
`scripts/benchmark.py` prints the numbers for humans; this keeps them from
|
|
4
|
+
silently decaying. The floors are set a little below the measured values so a
|
|
5
|
+
small, deliberate trade-off does not fail the build, but a real regression does.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
16
|
+
|
|
17
|
+
from agentcheckup.conflicts import ToolRef, detect_conflicts # noqa: E402
|
|
18
|
+
from agentcheckup.models import Tool # noqa: E402
|
|
19
|
+
|
|
20
|
+
FIXTURE = Path(__file__).resolve().parent / "fixtures" / "collision_benchmark.json"
|
|
21
|
+
|
|
22
|
+
MIN_PRECISION = 0.90
|
|
23
|
+
MIN_RECALL = 0.90
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_ref(side: dict) -> ToolRef:
|
|
27
|
+
tool = Tool.from_raw(
|
|
28
|
+
{
|
|
29
|
+
"name": side["name"],
|
|
30
|
+
"description": side["description"],
|
|
31
|
+
"inputSchema": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {"path": {"type": "string", "pattern": "^.*$"}},
|
|
34
|
+
"additionalProperties": False,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
return ToolRef(server=side["server"], tool=tool)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def score() -> tuple[float, float, list[str], list[str]]:
|
|
42
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
43
|
+
tp = fp = fn = 0
|
|
44
|
+
false_positives: list[str] = []
|
|
45
|
+
false_negatives: list[str] = []
|
|
46
|
+
|
|
47
|
+
for case in cases:
|
|
48
|
+
found = detect_conflicts([to_ref(case["a"]), to_ref(case["b"])])
|
|
49
|
+
predicted = bool(found)
|
|
50
|
+
if predicted and case["conflict"]:
|
|
51
|
+
tp += 1
|
|
52
|
+
elif predicted and not case["conflict"]:
|
|
53
|
+
fp += 1
|
|
54
|
+
false_positives.append(case["id"])
|
|
55
|
+
elif not predicted and case["conflict"]:
|
|
56
|
+
fn += 1
|
|
57
|
+
false_negatives.append(case["id"])
|
|
58
|
+
|
|
59
|
+
precision = tp / (tp + fp) if tp + fp else 0.0
|
|
60
|
+
recall = tp / (tp + fn) if tp + fn else 0.0
|
|
61
|
+
return precision, recall, false_positives, false_negatives
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_benchmark_precision_and_recall_hold():
|
|
65
|
+
precision, recall, false_positives, false_negatives = score()
|
|
66
|
+
assert precision >= MIN_PRECISION, f"precision dropped to {precision:.3f}; FP: {false_positives}"
|
|
67
|
+
assert recall >= MIN_RECALL, f"recall dropped to {recall:.3f}; FN: {false_negatives}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_benchmark_covers_both_labels():
|
|
71
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
72
|
+
positives = sum(1 for c in cases if c["conflict"])
|
|
73
|
+
negatives = len(cases) - positives
|
|
74
|
+
# A benchmark that drifts to one class measures nothing.
|
|
75
|
+
assert positives >= 10, positives
|
|
76
|
+
assert negatives >= 10, negatives
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_benchmark_includes_chinese_cases():
|
|
80
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
81
|
+
chinese = [c for c in cases if any("\u4e00" <= ch <= "\u9fff" for ch in c["a"]["name"])]
|
|
82
|
+
assert len(chinese) >= 4, len(chinese)
|
|
@@ -128,3 +128,40 @@ def test_audit_tokens_sum_across_servers():
|
|
|
128
128
|
tools = {"a": [tool("alpha_tool")], "b": [tool("beta_tool")]}
|
|
129
129
|
report = lint_audit(servers, tools)
|
|
130
130
|
assert report.tokens > 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --------------------------------------------------------------------------- #
|
|
134
|
+
# CJK support
|
|
135
|
+
# --------------------------------------------------------------------------- #
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_cjk_runs_become_character_bigrams():
|
|
139
|
+
assert tokens("读取文件") == {"读取", "取文", "文件"}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_single_cjk_character_is_kept_whole():
|
|
143
|
+
assert tokens("读") == {"读"}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_cjk_and_latin_mix():
|
|
147
|
+
got = tokens("read 文件")
|
|
148
|
+
assert "read" in got
|
|
149
|
+
assert "文件" in got
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_chinese_near_duplicates_are_flagged():
|
|
153
|
+
"""Before CJK support this pair produced two empty token sets and no finding."""
|
|
154
|
+
a = ref("文件服务", "读取文件", "读取指定路径的文件内容并返回文本")
|
|
155
|
+
b = ref("文档服务", "读取文档", "读取指定路径的文件内容并返回字符串")
|
|
156
|
+
assert "AGT203" in {c.rule for c in detect_conflicts([a, b])}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_chinese_unrelated_tools_are_not_flagged():
|
|
160
|
+
a = ref("文件服务", "读取文件", "读取指定路径的文件内容并返回文本")
|
|
161
|
+
b = ref("文件服务", "删除文件", "删除指定路径的文件")
|
|
162
|
+
assert detect_conflicts([a, b]) == []
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_english_scores_are_unchanged_by_cjk_support():
|
|
166
|
+
"""The CJK path must not touch Latin tokenisation."""
|
|
167
|
+
assert tokens("Read a file") == {"read", "file"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|