verifiable-agent-core 0.1.3__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.
- verifiable_agent_core-0.1.3/.gitattributes +10 -0
- verifiable_agent_core-0.1.3/.github/workflows/publish.yml +37 -0
- verifiable_agent_core-0.1.3/.github/workflows/test.yml +33 -0
- verifiable_agent_core-0.1.3/LICENSE +21 -0
- verifiable_agent_core-0.1.3/MANIFEST.in +11 -0
- verifiable_agent_core-0.1.3/MANIFEST.sha256 +31 -0
- verifiable_agent_core-0.1.3/PKG-INFO +507 -0
- verifiable_agent_core-0.1.3/README.md +459 -0
- verifiable_agent_core-0.1.3/adapters/openai_function.py +95 -0
- verifiable_agent_core-0.1.3/contrib/README.md +42 -0
- verifiable_agent_core-0.1.3/contrib/autogen_tool.py +33 -0
- verifiable_agent_core-0.1.3/contrib/crewai_tool.py +32 -0
- verifiable_agent_core-0.1.3/contrib/langchain_tool.py +40 -0
- verifiable_agent_core-0.1.3/contrib/llamaindex_tool.py +36 -0
- verifiable_agent_core-0.1.3/contrib/mcp_server.py +49 -0
- verifiable_agent_core-0.1.3/examples/README.md +39 -0
- verifiable_agent_core-0.1.3/examples/TASK.md +86 -0
- verifiable_agent_core-0.1.3/examples/bench.py +374 -0
- verifiable_agent_core-0.1.3/examples/data/replay/bare/hot.csv +12 -0
- verifiable_agent_core-0.1.3/examples/data/replay/bare/latest_count.csv +11 -0
- verifiable_agent_core-0.1.3/examples/data/replay/bare/report.md +30 -0
- verifiable_agent_core-0.1.3/examples/data/replay/bare/summary.csv +13 -0
- verifiable_agent_core-0.1.3/examples/data/replay/gated/hot.csv +12 -0
- verifiable_agent_core-0.1.3/examples/data/replay/gated/latest_count.csv +7 -0
- verifiable_agent_core-0.1.3/examples/data/replay/gated/report.md +32 -0
- verifiable_agent_core-0.1.3/examples/data/replay/gated/summary.csv +13 -0
- verifiable_agent_core-0.1.3/pyproject.toml +62 -0
- verifiable_agent_core-0.1.3/setup.cfg +4 -0
- verifiable_agent_core-0.1.3/tests/test_contract.py +404 -0
- verifiable_agent_core-0.1.3/tests/test_core.py +383 -0
- verifiable_agent_core-0.1.3/tools/check_manifest.py +68 -0
- verifiable_agent_core-0.1.3/vac/__init__.py +68 -0
- verifiable_agent_core-0.1.3/vac/gate.py +647 -0
- verifiable_agent_core-0.1.3/vac/verdict.py +143 -0
- verifiable_agent_core-0.1.3/verifiable_agent_core.egg-info/PKG-INFO +507 -0
- verifiable_agent_core-0.1.3/verifiable_agent_core.egg-info/SOURCES.txt +37 -0
- verifiable_agent_core-0.1.3/verifiable_agent_core.egg-info/dependency_links.txt +1 -0
- verifiable_agent_core-0.1.3/verifiable_agent_core.egg-info/requires.txt +30 -0
- verifiable_agent_core-0.1.3/verifiable_agent_core.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# 行尾统一 LF。
|
|
2
|
+
#
|
|
3
|
+
# 这一行不是风格问题。MANIFEST.sha256 存的是字节哈希,git 在 Windows 上检出时
|
|
4
|
+
# 默认会把文本文件的 LF 换成 CRLF —— 字节变了,哈希就整份对不上,
|
|
5
|
+
# 于是「校验失败」看起来像「文件被人改过」,实际只是换了个操作系统。
|
|
6
|
+
* text=auto eol=lf
|
|
7
|
+
|
|
8
|
+
# 清单自己是二进制:它记录的是别的文件的字节,自己绝不能被动过一个字符。
|
|
9
|
+
# 去掉这个 -text,清单会在 Windows 检出时变成 CRLF,然后连它自己的哈希都对不上。
|
|
10
|
+
MANIFEST.sha256 -text
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# 打 tag 就发版:git tag v0.1.3 && git push origin v0.1.3
|
|
4
|
+
#
|
|
5
|
+
# 一次性配置(只需做一次,在自己账号下点几下):
|
|
6
|
+
# 1. 去 https://pypi.org/manage/account/publishing/ 添加一个 Trusted Publisher
|
|
7
|
+
# - PyPI Project Name: verifiable-agent-core
|
|
8
|
+
# - Owner: 2409108080
|
|
9
|
+
# - Repository name: verifiable-agent-core
|
|
10
|
+
# - Workflow name: publish.yml
|
|
11
|
+
# - Environment name: pypi
|
|
12
|
+
# 2. 在 GitHub 仓库 Settings → Environments 建一个名叫 pypi 的环境
|
|
13
|
+
#
|
|
14
|
+
# 不用存 token、不用密码:OIDC 让 PyPI 认这个 workflow 的身份。
|
|
15
|
+
# 手打 token 上传过一次的东西,迟早会出现在某个 .env.bak 里。
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
tags: ["v*"]
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
build-and-publish:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi
|
|
26
|
+
permissions:
|
|
27
|
+
id-token: write # 拿 OIDC 身份,发 PyPI 的唯一凭据
|
|
28
|
+
contents: read
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: actions/setup-python@v5
|
|
32
|
+
with:
|
|
33
|
+
python-version: "3.12"
|
|
34
|
+
- run: python -m pip install --upgrade build
|
|
35
|
+
- run: python -m build
|
|
36
|
+
- run: python -m pip install dist/*.whl && python -c "import vac; print('import ok', vac.__name__)"
|
|
37
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
# 全家都用 UTF-8 跑。这不是洁癖:三个 job 之前永远在 windows-latest 上红,
|
|
8
|
+
# 原因是 examples/bench.py 和测试里的子进程都会往 stdout 打中文,而 en-US 的
|
|
9
|
+
# Windows 控制台代码页是 cp1252 —— 编不出来的字符会直接把进程打挂(返回码 1),
|
|
10
|
+
# 于是「跑个 demo」这一步看起来像代码坏了。代码没坏,代码页坏了。
|
|
11
|
+
# 判据不该依赖跑它的那台机器是哪个语言版本。
|
|
12
|
+
env:
|
|
13
|
+
PYTHONUTF8: "1"
|
|
14
|
+
PYTHONIOENCODING: "utf-8"
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
23
|
+
python: ["3.10", "3.12", "3.13"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python }}
|
|
29
|
+
- run: python -m pip install -e ".[dev]"
|
|
30
|
+
- run: python -m pytest -q
|
|
31
|
+
- run: python -m ruff check .
|
|
32
|
+
- run: python examples/bench.py --gates
|
|
33
|
+
- run: python examples/bench.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Verifiable-Agent-Core Contributors
|
|
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,11 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
include MANIFEST.sha256
|
|
5
|
+
include .gitattributes
|
|
6
|
+
recursive-include adapters *.py
|
|
7
|
+
recursive-include contrib *.py *.md
|
|
8
|
+
recursive-include examples *.py *.md *.csv
|
|
9
|
+
recursive-include tests *.py
|
|
10
|
+
recursive-include tools *.py
|
|
11
|
+
recursive-include .github *.yml
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
4584bceff56310a4914c7828928324184110d3a72c874818d7c479ca5e87c089 .gitattributes
|
|
2
|
+
6c91bc9bdc6c2ef826e70b053c8b9ae30a807e354e3bac2ff52205b9006f5b79 .github/workflows/test.yml
|
|
3
|
+
0c111f882d894034de28d5fef070a6151e271aca4e4f3b2af3394fb53143f5b6 .gitignore
|
|
4
|
+
c1cdbd24336162e08fd98f0eef36cbe92720bac0fa7b78d41ee5366648cac4ed LICENSE
|
|
5
|
+
44f16cb95e4c2eb86d63b5b8afb6af088fe12754d4e3cc119f425829827b36f0 MANIFEST.in
|
|
6
|
+
3bc4bd3d23c89d8a82ccf171c723eed64a852d94dc97a7afe3895c86e7aaa006 README.md
|
|
7
|
+
918b3d2b342e3c5d86dec8d32923198c9c106ed87fffb4f0ff9cd5d7f63abf17 adapters/openai_function.py
|
|
8
|
+
3c3abc0a8a9b4f2a4f760b6ae4db3628c63d06ca7fb37db17ba461bfd15abb2d contrib/README.md
|
|
9
|
+
391fbd568c597de962b9d0c96a0359a1df2fcada88139c4c5a0cdb32e0d21962 contrib/autogen_tool.py
|
|
10
|
+
6f685d22a5c172d7d6c23c5b95615a349530929c85659df3fcc1a7e6b4c0e2a9 contrib/crewai_tool.py
|
|
11
|
+
cf039c92e75d73fc62131b2eee151a71899ac621d30ac7ded92e17b070960f9a contrib/langchain_tool.py
|
|
12
|
+
81557f8777675fe1d948d8cafa02b2c15760a9e3a58e3eacbd60aeeab92443bd contrib/llamaindex_tool.py
|
|
13
|
+
8931e0bfb28a8f21338240bb003651dd3aba2892ae55e9607ea87808b3faa307 contrib/mcp_server.py
|
|
14
|
+
feaccb92b044e11e001487dc502b3004796ac8b4b1e75d4c2e891fce194c8ef8 examples/README.md
|
|
15
|
+
db2fb41118b2c0c630c5bc01fc5396e9cddbcadffb0ce1c23fb21918a32a8168 examples/TASK.md
|
|
16
|
+
91d09773f33e4fc36624670f368571fa64cbba4b55b6dd086c1dfb3b77cf2640 examples/bench.py
|
|
17
|
+
f32435ebdeb9bcf5aed5eb27b64dbb5020c5dce8149807b4f187ce1a0a0c19f9 examples/data/replay/bare/hot.csv
|
|
18
|
+
deec8d234c2e26e3351140cd02ad79522397f55acc48f1e6bc5a06d3083562fb examples/data/replay/bare/latest_count.csv
|
|
19
|
+
d4d6bdb362d56cd83b1e4043acd26933c4ed718044a6682c21c26d3ee0fa7d75 examples/data/replay/bare/report.md
|
|
20
|
+
992c513cd5dd66e06dd1a95840937d358b4982b586355164492722b9861e0d1b examples/data/replay/bare/summary.csv
|
|
21
|
+
48374bf2898a3365f9b978d32544d889b393d422bd6f65229a8314d8a5cd4069 examples/data/replay/gated/hot.csv
|
|
22
|
+
d3d82c7312704c6bf8235909cc5dc2a60a8e520effc9b902648a7052789e11f6 examples/data/replay/gated/latest_count.csv
|
|
23
|
+
44fddf1c99731f53ffe2d8d8daf9b705f01a1f7903cf342d869869665f4fb021 examples/data/replay/gated/report.md
|
|
24
|
+
992c513cd5dd66e06dd1a95840937d358b4982b586355164492722b9861e0d1b examples/data/replay/gated/summary.csv
|
|
25
|
+
5ed95058180c9a756efa23751a252db876b3b1bfad7c3acb6da699173aa65230 pyproject.toml
|
|
26
|
+
c3ab47d8040f532625be2b4a0d88428ce86708dc960faa7a486c7d35ff1aaf25 tests/test_contract.py
|
|
27
|
+
30e412e45b0cbfcad35047d9f102e35f11ca4ead9c648f1ccbe8121780b5094e tests/test_core.py
|
|
28
|
+
6f9d1bd201c6c82907073c2cd0e50c52521571109fb626fd39e4cd27a65bcf76 tools/check_manifest.py
|
|
29
|
+
f28a5902c06e50049024104c936b026f6cc12869f493fc5cc02d8b7ca01b8b0c vac/__init__.py
|
|
30
|
+
5b3e4f926a687251206c88876398463588d0210306c100d0faab61f3e5d4dcd0 vac/gate.py
|
|
31
|
+
795a2d8efe11761b3a0ce062408ae77c0dbbd67c44d572477570fb2e9fa412a4 vac/verdict.py
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verifiable-agent-core
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Make an agent's "I wrote it" fail loudly: a stdlib-only delivery gate that refuses wrong filenames, empty or zero-width files and empty-shell overwrites, then verifies every byte by read-back.
|
|
5
|
+
Author: Verifiable-Agent-Core Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/2409108080/verifiable-agent-core
|
|
8
|
+
Project-URL: Repository, https://github.com/2409108080/verifiable-agent-core
|
|
9
|
+
Project-URL: Issues, https://github.com/2409108080/verifiable-agent-core/issues
|
|
10
|
+
Keywords: agent,llm,agent-reliability,hallucination,tool-call,verification,delivery,atomic-write,guardrail,fsync
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: System :: Filesystems
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
28
|
+
Provides-Extra: langchain
|
|
29
|
+
Requires-Dist: langchain-core>=0.2; extra == "langchain"
|
|
30
|
+
Provides-Extra: llamaindex
|
|
31
|
+
Requires-Dist: llama-index-core>=0.11; extra == "llamaindex"
|
|
32
|
+
Provides-Extra: crewai
|
|
33
|
+
Requires-Dist: crewai>=0.30; extra == "crewai"
|
|
34
|
+
Provides-Extra: autogen
|
|
35
|
+
Requires-Dist: autogen-core>=0.4; extra == "autogen"
|
|
36
|
+
Provides-Extra: mcp
|
|
37
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
38
|
+
Provides-Extra: openai
|
|
39
|
+
Requires-Dist: openai>=1.30; extra == "openai"
|
|
40
|
+
Provides-Extra: all
|
|
41
|
+
Requires-Dist: langchain-core>=0.2; extra == "all"
|
|
42
|
+
Requires-Dist: llama-index-core>=0.11; extra == "all"
|
|
43
|
+
Requires-Dist: crewai>=0.30; extra == "all"
|
|
44
|
+
Requires-Dist: autogen-core>=0.4; extra == "all"
|
|
45
|
+
Requires-Dist: mcp>=1.0; extra == "all"
|
|
46
|
+
Requires-Dist: openai>=1.30; extra == "all"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# Verifiable-Agent-Core
|
|
50
|
+
|
|
51
|
+
[](https://github.com/2409108080/verifiable-agent-core/actions/workflows/test.yml)
|
|
52
|
+
[](https://github.com/2409108080/verifiable-agent-core)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
|
|
55
|
+
**A ~600-line delivery gate for agents. It turns "I wrote it" from a self-report into a call that can fail.**
|
|
56
|
+
|
|
57
|
+
No framework. No model calls. No API key. Standard library only. `import` and go.
|
|
58
|
+
|
|
59
|
+
> 你让 agent 跑一晚上交文件,第二天早上打开文件夹,里面应该只有你要的那几份东西。
|
|
60
|
+
> 名字不对、内容是空壳、只有零宽字符、旧账被空壳覆盖 —— 四种都在写入路径上被拒。
|
|
61
|
+
> **拒了那一次,盘上一个字节都没动。**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install verifiable-agent-core # 不是 `vac`:那个名字是别人的包
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from vac import Workspace, WriteRefused
|
|
69
|
+
|
|
70
|
+
ws = Workspace("work", required_files=["report.md", "summary.csv"]) # 名单只由部署方给
|
|
71
|
+
|
|
72
|
+
ws.write_and_verify("report.md", "# 本周复盘\n\n总作品 25 件。\n") # → Receipt(verified=True)
|
|
73
|
+
ws.verify_all_delivered() # False:还欠 summary.csv
|
|
74
|
+
ws.write_and_verify("README_final.md", "...") # → WriteRefused(delivery)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 边界:它只挡一个动词
|
|
78
|
+
|
|
79
|
+
**`write_file`。** 只要它是你给 Agent 的唯一落盘路径,这把锁就是闭合的 —— 名字不对、
|
|
80
|
+
内容空、旧账被空壳盖掉,都在调用返回之前被拒,模型只能拿着拒绝理由改对再来。
|
|
81
|
+
|
|
82
|
+
**如果 Agent 还有第二条落盘的路,本库既拦不住也看不见**:shell、代码沙箱、浏览器下载、
|
|
83
|
+
你自己加的文件工具、同一个 MCP 里另挂的文件系统 server,都算。这不是藏起来的缺陷,是
|
|
84
|
+
适用范围 —— **接线只接一个写口,闸才叫闸;接了两个,另外那个就是正门。**
|
|
85
|
+
|
|
86
|
+
同理,它不判断内容对不对:四段「待补充」凑够 `min_bytes`,四道闸全绿。下限保证的是
|
|
87
|
+
「像份东西」,不是「东西对」——**那是评分器的活,别指望闸替你判对错。**
|
|
88
|
+
|
|
89
|
+
> ⚠️ **V0.1.3(实验性)**:连 1.0 都还没到。四道闸可用,但多进程并发、跨机器、
|
|
90
|
+
> 第二条写入路径这些边界都没解决 —— 先看完这一节再决定要不要用。
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 它不做什么(逐条,别跳)
|
|
95
|
+
|
|
96
|
+
- **不提高模型能力。** 它只在交付这一环设卡。推理错了、数据算错了,它拦不住。
|
|
97
|
+
- **不管思考过程。** 没有规划器、没有记忆、没有反思循环。那些是 Agent 框架的事。
|
|
98
|
+
- **只管文件名,不管路径。** 契约点名 `report.md`,写到 `deep/nested/report.md`
|
|
99
|
+
**也算已交付** —— 匹配用的是 basename。这是刻意的克制(不做全路径推理,就不会有
|
|
100
|
+
「我觉得这个路径等价」这种模糊地带),代价是路径伪装拦不住。**本库没有位置校验**:
|
|
101
|
+
想知道某一次到底写到了哪,看 `Receipt.path`,别指望闸替你判断目录对不对。
|
|
102
|
+
- **空文件、占位文件都会被拒,但「有内容」不等于「内容对」。** 空/纯空白无条件拒绝;
|
|
103
|
+
契约还能要求 `min_bytes` / `min_lines`。再往上一层的对错(数据算得对不对、结论站不站得住)
|
|
104
|
+
它管不了 —— 那是评分器的事。
|
|
105
|
+
- **交齐不等于解封,但 `end_task()` 之后就真不管了。** 清单交齐之后,写清单外的名字
|
|
106
|
+
仍然被拦 —— 放开权限的动作只有一个(`end_task()`),而且在部署方手里。代价是调用它
|
|
107
|
+
之后本实例不再有任何契约:后续写入不受管,`verify_all_delivered()` 也会变成 `False`
|
|
108
|
+
(没契约可验)。**验收请用 `end_task()` 的返回值**,要接着做下一件事就 `init_task()`。
|
|
109
|
+
- **不是安全沙箱,是一个「可验证执行层」。** `resolve()` 只防路径跑出工作区
|
|
110
|
+
(含符号链接/junction),不防恶意代码。**Agent 如果拥有终端权限,可以直接绕过本库**
|
|
111
|
+
—— 自己 `open()` 写盘、删掉文件、改进程内存,都拦不住。在不可信环境里跑,请套真正的容器。
|
|
112
|
+
- **只保证单进程写入校验,不保证多进程并发下的数据强一致性。** 「读旧文件 → 判断 → 落盘」
|
|
113
|
+
之间有时间窗(TOCTOU):两个进程同时写同一个文件,后写的赢,先写的那次回执仍然有效。
|
|
114
|
+
要多进程安全,请在闸外面加文件锁(Windows `msvcrt.locking` / POSIX `fcntl.flock`)。
|
|
115
|
+
- **不证明性能提升。** `examples/` 里的 93:100 是**历史存档复算(n=1 单局)**:
|
|
116
|
+
同一道题、同一个裁判重算一遍。它不是 A/B 统计实验,不能当作提升幅度的绝对统计证明。
|
|
117
|
+
- **不防同进程内的代码执行。** 契约的「不可变」是 Python 对象级的:能在同一进程里跑任意
|
|
118
|
+
Python 的模型,可以直接改内存、改 `os.environ` 的同时改快照。那种威胁模型下必须把校验
|
|
119
|
+
放到进程外(另一台机器、另一个用户、只读挂载或独立审计进程)。
|
|
120
|
+
- **不替你判对错。** `judge_3state` 只保证「判不出来」不会被记成「成了」;
|
|
121
|
+
至于 `True` 是不是真的对,取决于你喂进去的判据。
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 它修的是一个具体的病
|
|
126
|
+
|
|
127
|
+
Agent 跑完任务,回来跟你说「已完成,报告写在 report.md」。你去盘上看,有三种情况:
|
|
128
|
+
|
|
129
|
+
1. 文件不在 —— 它写到了自己顺手编的名字(`README_final.md`、`note.md`);
|
|
130
|
+
2. 文件在,但里面是个空壳 —— 它把一份 10 条数据的热榜覆盖成了一行表头;
|
|
131
|
+
3. 文件在,内容也对 —— 但那是上一轮留下的,这一轮根本没写成。
|
|
132
|
+
|
|
133
|
+
这三种在返回值里长得**一模一样**:`ok: true`。
|
|
134
|
+
|
|
135
|
+
提示词治不了这个病。你在 prompt 里加十遍「必须写到 report.md」,它下一次采样
|
|
136
|
+
仍然可能写别的名字 —— 因为 prompt 是建议,采样是概率。**能治的只有拒绝**:
|
|
137
|
+
写错名字这一次调用就直接失败,模型拿到失败原因,重来。
|
|
138
|
+
|
|
139
|
+
这就是本库唯一的野心:把「交付」从一句自我声明,变成一个**会失败的调用**。
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 四个函数 + 一道验收
|
|
144
|
+
|
|
145
|
+
| 入口 | 管什么 | 失败时 |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `Workspace(root, required_files=[...], min_bytes=, min_lines=)` / `init_task(names)` | 任务点名要交哪些文件、每份至少多大(**只有开发者能设**) | 写别的名字 → `WriteRefused("delivery")`;不到下限 → `WriteRefused("too_small")` |
|
|
148
|
+
| `guard_overwrite(path, text)` | 旧文件有真数据,新内容是空壳(`allow_shrink=True` 可关掉这道) | 拒绝覆盖 → `WriteRefused("overwrite")`,盘上原样不动 |
|
|
149
|
+
| `write_and_verify(path, text)` | 空内容检查 + 原子写盘 + 读回逐字节比对 | 空/纯空白 → `WriteRefused("empty")`;对不上 → `WriteNotVerified`,**不返回回执** |
|
|
150
|
+
| `judge_3state(outcome)` | 成了 / 没成 / 判不出来 | `None` 永远是 `unknown`,`if` 里为假 |
|
|
151
|
+
| `verify_all_delivered()` | 验收:点名的都在盘上、没被调包、够得着下限 | 返回 `False`(不抛),只认盘上的字节;没有契约也返回 `False` |
|
|
152
|
+
| `end_task()` | **唯一**能解除写入限制的钩子(只有开发者能调) | 返回本次任务的验收快照 |
|
|
153
|
+
|
|
154
|
+
`write_and_verify` 内部按顺序串了四道闸(交付契约 → 空内容 → 防误删锁 → 读回校验),
|
|
155
|
+
所以正常只需要调它一个。
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 30 秒上手
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git clone https://github.com/2409108080/verifiable-agent-core
|
|
163
|
+
cd verifiable-agent-core && pip install -e .
|
|
164
|
+
# 只想先看一眼:在仓库根目录起 REPL 可以 import vac(纯 stdlib,零依赖);
|
|
165
|
+
# 跑 examples/ 请先安装 —— 脚本另有路径兜底,但装过才是标准姿势
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from vac import Workspace, judge_3state, WriteRefused
|
|
170
|
+
|
|
171
|
+
# 名单在**构造时**给。这一步之后它只读,没有 API 能改宽或清空。
|
|
172
|
+
ws = Workspace("workspace", required_files=["report.md", "summary.csv"])
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
r = ws.write_and_verify("report.md", "# 本周复盘\n\n总作品 25 件。\n")
|
|
176
|
+
print(r.verified, r.bytes, r.sha256[:8]) # True 36 40d876d7
|
|
177
|
+
except WriteRefused as e:
|
|
178
|
+
print("被拒:", e.reason, e.detail) # delivery / overwrite / escape / empty
|
|
179
|
+
|
|
180
|
+
ws.verify_all_delivered() # False —— 还欠 summary.csv
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`Receipt.verified` 是构造时就为 `True` 的 —— 拿得到回执,就说明读回来了、且一字不差。
|
|
184
|
+
|
|
185
|
+
但 `Receipt.delivery_complete` 是另一回事,别混着读:它只回答「契约上的文件是不是全齐了」。
|
|
186
|
+
**没有契约时它恒为 `False`**(跟 `verify_all_delivered()` 同一条判据)—— 没设清单就没有
|
|
187
|
+
「齐了」这回事,一次普通写入不该拿到盖着「交付完成」的章。
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 看它拦一次
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
python examples/bench.py # 真跑,不调模型,秒级
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
任务:把本周复盘交成 report.md 与 summary.csv。
|
|
199
|
+
(名单是部署方在构造 Workspace 时写死的,模型手里没有任何改它的工具)
|
|
200
|
+
|
|
201
|
+
──────────────────────────────────────────────────────────────────
|
|
202
|
+
第 1 次:模型自认为文件名「更好看」,写成 README_final.md
|
|
203
|
+
──────────────────────────────────────────────────────────────────
|
|
204
|
+
[model] write_file(path="README_final.md", ...)
|
|
205
|
+
[vac] ✗ WriteRefused(reason=delivery)
|
|
206
|
+
任务点名的交付文件还缺:report.md、summary.csv。要求的文件名是 report.md,
|
|
207
|
+
不准自创「README_final.md」。先把点名的交齐,再写别的。
|
|
208
|
+
[disk] workspace/ 里现在有:(空,一个字节都没落)
|
|
209
|
+
|
|
210
|
+
──────────────────────────────────────────────────────────────────
|
|
211
|
+
第 2 次:模型照拒绝理由改成 report.md
|
|
212
|
+
──────────────────────────────────────────────────────────────────
|
|
213
|
+
[model] write_file(path="report.md", ...)
|
|
214
|
+
[vac] ✓ 写入并读回:36 字节 / 3 行 / sha256 40d876d7cd44…
|
|
215
|
+
[vac] 交付进度:['summary.csv'] 还缺
|
|
216
|
+
|
|
217
|
+
──────────────────────────────────────────────────────────────────
|
|
218
|
+
第 4 次:交齐了两份,模型想再塞一份 notes.md(旧版本这里会放行)
|
|
219
|
+
──────────────────────────────────────────────────────────────────
|
|
220
|
+
[model] write_file(path="notes.md", ...)
|
|
221
|
+
[vac] ✗ WriteRefused(reason=delivery)
|
|
222
|
+
清单上的文件都交齐了,但任务还没结束(未调用 end_task()),
|
|
223
|
+
这里仍然只认清单内的名字,不准写「notes.md」。
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
上面每一行 `[vac]` 都是 `WriteRefused` 的原文,`[disk]` 是当场列出来的目录内容 ——
|
|
227
|
+
`demo_intercept()` 里没有任何一句是写死的旁白。
|
|
228
|
+
|
|
229
|
+
要点只有一句:**拒绝发生在写入路径上**。模型没法「说服」它放行,
|
|
230
|
+
只能拿着拒绝理由改对再来一次;而失败的那一次,一个字节都没落盘。
|
|
231
|
+
|
|
232
|
+
`python examples/bench.py --gates` 是另一组演示:空壳覆盖、空内容、读回不一致,四道闸各拦一次。
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 权限模型:钥匙不在被锁的人手里
|
|
237
|
+
|
|
238
|
+
这一节是这个库和「提示词里写纪律」的全部区别。
|
|
239
|
+
|
|
240
|
+
**病根**:如果模型手里有一个「登记交付清单」的工具,它就能先调
|
|
241
|
+
`set_delivery("")` 把闸拆了,再随便写。锁装在了门的同一侧 —— 这不是锁不结实,
|
|
242
|
+
是根本没锁。第一版就是这么错的,`set_delivery` 曾是模型可调的工具。
|
|
243
|
+
|
|
244
|
+
**现在的结构**:
|
|
245
|
+
|
|
246
|
+
- **名单只能从开发者通道进**:`Workspace(root, required_files=[...])`,或任务开始时
|
|
247
|
+
调 `init_task([...])`。两个都不在任何适配器的工具表里。
|
|
248
|
+
- **设完即封印**:契约是 `frozen` 的 `Contract` 对象,清单是 `tuple`。
|
|
249
|
+
没有 `set_delivery`、没有 `clear`、没有「传空字符串」这一说。
|
|
250
|
+
- **重设即异常**:任务没结束就再调 `init_task()`(无论谁调、传什么都算),
|
|
251
|
+
抛 `ContractViolation`。要换任务,开发者先 `end_task()`,再 `init_task()`。
|
|
252
|
+
- **空清单直接拒绝**:`required_files=[]`、`[""]`、`init_task([])` 全部抛
|
|
253
|
+
`ContractViolation` —— 「什么都不用交」等于给所有交付盖章,它不是一种任务。
|
|
254
|
+
- **改环境变量也算篡改**:封印瞬间会把 `VAC_DELIVERY` 快照记进契约;之后每次
|
|
255
|
+
写入 / 验收都比对一次。改宽、改窄、删掉,一律 `ContractViolation`。
|
|
256
|
+
- **验收只认盘上的字节**:`verify_all_delivered()` 重新读文件、比 sha256,
|
|
257
|
+
不信内存里那句「我写过了」。
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
ws = Workspace("workspace", required_files=["report.md"])
|
|
261
|
+
ws.init_task([]) # ContractViolation:已封印,任务没结束
|
|
262
|
+
ws.contract_names.append("x.md") # AttributeError:tuple
|
|
263
|
+
ws.contract.names = () # FrozenInstanceError:frozen dataclass
|
|
264
|
+
os.environ["VAC_DELIVERY"] = "" # 下一次写入 → ContractViolation
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
模型能做的只剩一件事:调 `write_file` 往工作区里写。名字不对,这一次调用失败。
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 四道闸 + 一条判据,各拦一种真事故
|
|
272
|
+
|
|
273
|
+
**① 写错名字。** 任务要 `report.md`,它写了 `README_final.md`,内容全对。
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
ws = Workspace("workspace", required_files=["report.md"])
|
|
277
|
+
ws.write_and_verify("README_final.md", "...") # 内容再对也不收
|
|
278
|
+
# WriteRefused: [delivery] README_final.md: 任务点名的交付文件还缺:report.md。
|
|
279
|
+
# 要求的文件名是 report.md,不准自创「README_final.md」。
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
盘上一个字节都没动。模型拿到这句话,下一次就会写对 —— 这不是说服,是没得选。
|
|
283
|
+
|
|
284
|
+
**② 防误删锁:防的是手抖,不是恶意的渐进式覆盖。**
|
|
285
|
+
|
|
286
|
+
`hot.csv` 里躺着 10 条真实话题,它要写一行表头进去。
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
ws = Workspace("workspace") # 这个任务不做交付约束,单看这一道闸
|
|
290
|
+
# 旧文件 ≥4 行,且行数或字节塌掉一半以上(两条任一成立就拦)
|
|
291
|
+
ws.write_and_verify("hot.csv", "rank,topic,heat\n")
|
|
292
|
+
# WriteRefused: [overwrite] 这里存着 11 行内容(317 字节),你这次只写了 1 行(16 字节)。
|
|
293
|
+
# 行数从 11 掉到 1(不到一半)。
|
|
294
|
+
# 没有新数据就把旧账擦掉是自毁 —— 不许。
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
正常改写(相仿或更长)一律放行。判据用**新旧比例**而不是「新内容 ≤N 行」:
|
|
298
|
+
绝对阈值填两行长注释就能绕过(11 行真账 → 1 行表头 + 2 行废话,字节还变多了)。
|
|
299
|
+
代价是「11 行 CSV 原地改写成 1 行 JSON」会被拦 —— 这种改写请另存新名,
|
|
300
|
+
或者由开发者在构造时写 `Workspace(..., allow_shrink=True)` 主动关掉这一道(默认 `False`)。
|
|
301
|
+
|
|
302
|
+
它防的也不是「恶意的渐进式覆盖」:每轮删 10%,每轮都不到一半,这条锁永远不会响。
|
|
303
|
+
要防那种,得靠版本控制或 append-only 日志 —— 本库不假装自己能干这个。
|
|
304
|
+
|
|
305
|
+
**③ 空文件当交付。** 最便宜的一种假装:`write_and_verify("report.md", "")`。
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
ws.write_and_verify("report.md", "") # WriteRefused: [empty] 内容里没有任何可见字符。
|
|
309
|
+
ws.write_and_verify("report.md", " \n\t\n") # 一样拒 —— 空白不是内容
|
|
310
|
+
ws.write_and_verify("report.md", "\u200b") # 零宽空格同样是空文件(0.1.2 补的)
|
|
311
|
+
# 契约还能加下限:占位文件、只写一句「已完成」都算没交
|
|
312
|
+
ws = Workspace("workspace", required_files=["report.md"], min_bytes=200, min_lines=5)
|
|
313
|
+
# WriteRefused: [too_small] 契约要求这份交付的内容(至少 200 字节、5 行),至少要 200 字节…
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
这一条跟有没有契约无关,是写入层的规矩。0.1.0 里它漏了,一个 0 字节的 `report.md`
|
|
317
|
+
能拿到 `verified=True` 的回执 —— 外面那轮盲测就是这么把包拆开的。
|
|
318
|
+
|
|
319
|
+
**④ 写了但没成。** 这是最隐蔽的一种:`os.replace` 返回了,`write()` 没报错,但盘上不是那个东西。
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
r = ws.write_and_verify("report.md", text) # 内部:mkstemp → write → fsync → replace → 读回
|
|
323
|
+
# 读回字节 != 写入字节 → 抛 WriteNotVerified,不给回执
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
所以本库**没有** `ok` 这个字段。只有 `Receipt`,或者异常。中间态不存在。
|
|
327
|
+
|
|
328
|
+
**⑤ 判不出来。** 三态的关键不是多一个值,是 `None` 不会被顺手当成成功:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
v = judge_3state(tool_result)
|
|
332
|
+
if v: # 只有 passed 才进来;unknown 和 failed 都走 else
|
|
333
|
+
ship_it()
|
|
334
|
+
|
|
335
|
+
judge_3state(None).state # "unknown"
|
|
336
|
+
bool(judge_3state(None)) # False ← 语言层兜底,不靠调用方守规矩
|
|
337
|
+
judge_3state({"ok": 1}).state # "unknown" ← 1 == True,但不是 bool,不认
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 接进你现有的 Agent
|
|
343
|
+
|
|
344
|
+
适配器是独立文件,20~35 行,复制到你的项目里就能用。名单由部署方在
|
|
345
|
+
**进程启动前**给:环境变量 `VAC_DELIVERY="report.md,summary.csv"`,或直接改那一行
|
|
346
|
+
`Workspace(..., required_files=[...])`。下限也走同一条通道:`VAC_MIN_BYTES=200`、
|
|
347
|
+
`VAC_MIN_LINES=5`(只约束点名的文件;没有 `VAC_DELIVERY` 时会在 stderr 喊一声,
|
|
348
|
+
因为那时它不会起作用)。适配器里没有改契约的工具 —— 这是故意的。
|
|
349
|
+
|
|
350
|
+
**只维护一个适配器:**
|
|
351
|
+
|
|
352
|
+
| 适配器 | 接法 | 状态 |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| [`adapters/openai_function.py`](adapters/openai_function.py) | 纯 JSON schema,DeepSeek / Qwen / Moonshot / OpenAI 通用 | **真机跑过**:拦截、放行、交齐后拒绝、`end_task()` 解封四条分支都有实跑输出 |
|
|
355
|
+
|
|
356
|
+
其余框架的包装都挪进了 [`contrib/`](contrib/),**除 LangChain 外均未经真机验证,仅供代码参考**:
|
|
357
|
+
|
|
358
|
+
| 参考实现 | 接法 | 状态 |
|
|
359
|
+
|---|---|---|
|
|
360
|
+
| [`contrib/langchain_tool.py`](contrib/langchain_tool.py) | `@tool` 装饰器 | 真机跑过(langchain-core 1.7.4) |
|
|
361
|
+
| [`contrib/llamaindex_tool.py`](contrib/llamaindex_tool.py) | `FunctionTool.from_defaults` | 未验证 |
|
|
362
|
+
| [`contrib/crewai_tool.py`](contrib/crewai_tool.py) | `@tool("write_file")` | 未验证 |
|
|
363
|
+
| [`contrib/autogen_tool.py`](contrib/autogen_tool.py) | `FunctionTool(...)` | 未验证 |
|
|
364
|
+
| [`contrib/mcp_server.py`](contrib/mcp_server.py) | MCP stdio 服务 | 未验证 |
|
|
365
|
+
|
|
366
|
+
宁可只给一个真能跑的,也不给六个只过了语法检查的包装 —— 那六个里每一个都能
|
|
367
|
+
让你以为「接上了」,然后在第一次真拦的时候才发现它压根没跑过。
|
|
368
|
+
|
|
369
|
+
共同的一条:**适配器只暴露 `write_file` 一个写入口**(MCP 那个另有一个只读的
|
|
370
|
+
`delivery_state`)。不暴露 `open()`、不暴露 `Path.write_text()` ——
|
|
371
|
+
有一个绕过校验的写口,闸就等于不存在。
|
|
372
|
+
|
|
373
|
+
### 旁路免责(重要)
|
|
374
|
+
|
|
375
|
+
**VAC 只能保护走 `write_file` 的通道。如果你给 Agent 开了别的文件系统直写权限,
|
|
376
|
+
本库不背锅。**
|
|
377
|
+
|
|
378
|
+
具体说,下面这些路都不经过闸,本库既拦不住也看不见:
|
|
379
|
+
|
|
380
|
+
- 你自己加的其他文件工具(`write_text` / `open(..., "w")` / `shutil` / `os.rename`);
|
|
381
|
+
- Agent 自带的代码执行、Shell、Jupyter、浏览器下载落盘;
|
|
382
|
+
- 同一个 MCP 里另外挂的文件系统 server;
|
|
383
|
+
- 任何在进程内能执行任意 Python 的模型(它可以直接改内存,见下节)。
|
|
384
|
+
|
|
385
|
+
接进生产前请自查:**Agent 的工具表里,除了 `write_file`,还有没有第二个能落盘的入口。**
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 为什么这么设计
|
|
390
|
+
|
|
391
|
+
**契约用封印,不用「约定」。** 上一版把 `set_delivery` 当工具给模型,理由是「换任务方便」。
|
|
392
|
+
代价是模型可以用一行调用把闸关掉 —— 便利给错了人。现在换任务仍然方便,只是那个动作
|
|
393
|
+
回到了部署方手里(`end_task()` → `init_task()`)。核心也因此从 499 行涨到 671 行:
|
|
394
|
+
多出来的 170 余行几乎全在「封印 + 篡改检测 + 只认盘上字节的验收」上(另有几行是修回执假绿灯),
|
|
395
|
+
没有一行业务逻辑。
|
|
396
|
+
|
|
397
|
+
**拒绝用异常,不用返回值。** 返回值会被 `if not ok: continue` 顺手吞掉;异常必须被处理。
|
|
398
|
+
被拒绝和写坏了是两种事故,所以是两个异常类,`reason` 是机器可判的短码。
|
|
399
|
+
|
|
400
|
+
**闸自己出故障时放行。** 路径解析、行数计算、读取旧文件全部包在 try 里,出错就过。
|
|
401
|
+
锁的 bug 不能反过来把人锁在门外 —— 安全装置的失效方向必须是「不拦」,不是「全拦」。
|
|
402
|
+
|
|
403
|
+
**关掉锁不影响写入。** `Workspace(locks=False)` 就是去掉这层锁的同一个 Agent:
|
|
404
|
+
一样的写路径、一样的读回校验,只是不再拒绝。
|
|
405
|
+
|
|
406
|
+
但它**不再是干净的对照组**(0.1.2 起说明白):空内容检查与锁无关,`locks=False`
|
|
407
|
+
也照拒空文件;契约下限则跟着 `locks` 一起下线。能对照的只有「防误删 + 交付契约 +
|
|
408
|
+
下限」这三样一起消失。要做 A/B,就得把两边的差别说全 —— 「提升多少」这个数应该是
|
|
409
|
+
跑出来的,不是写的。
|
|
410
|
+
|
|
411
|
+
**只有 stdlib。** 闸要在任何环境里都能立起来。为了一个 try/except 引入依赖,
|
|
412
|
+
等于在地基里埋一个可能装不上的东西。
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## 附录:那局 93 : 100(历史存档,不是核心卖点)
|
|
419
|
+
|
|
420
|
+
> **注意:此分数为历史存档复算,物理闸不提升模型智商,只强制交付纪律。**
|
|
421
|
+
|
|
422
|
+
`python examples/bench.py --bench` 复算的是另一件事:一道 30 分钟的数据分析题,
|
|
423
|
+
四个交付文件,两个陷阱(T+8min 热榜服务真断网、T+13min 递来一份「总数 31」的假更正,
|
|
424
|
+
真账是 25)。同一个 Agent 跑两遍,**唯一差别是这层闸**。
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
==== A 组 · 裸 Agent(无闸) 得分 93/100 ====
|
|
428
|
+
正常·report.md: 23/30 总数✓ 总赞✓ 最高作品✓ 核对差异✗
|
|
429
|
+
==== B 组 · 带 vac 物理闸 得分 100/100 ====
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**这是 n=1 的单局存档复算,不是统计意义上的提升幅度。** 7 分差在一处:
|
|
433
|
+
裸 Agent 算对了所有数,但没核对「备份 23 件 vs 真账 25 件」这个差异。
|
|
434
|
+
它没有写错文件、没有编数据 —— 它只是漏了一次核验。
|
|
435
|
+
|
|
436
|
+
> **注意:7 分差距是因为裸 Agent 少写了一句核对差异,这是模型智商问题,不是 vac 物理闸的功劳。
|
|
437
|
+
> 物理闸的价值在于强制交付,而不是让模型变聪明。**
|
|
438
|
+
>
|
|
439
|
+
> 换句话说:这一局换成任何足够仔细的模型,裸跑也能拿 100。闸不生产正确,
|
|
440
|
+
> 它只让「没交、没核对、写了空壳」这三类事在验收时发出声音。
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## 0.1.1:三个 P0(外面那轮盲测把包拆开的地方)
|
|
445
|
+
|
|
446
|
+
0.1.0 发出去之后被做了一轮盲测,三个洞都是真的。修法都写在上面的四道闸里,这里只留证据:
|
|
447
|
+
|
|
448
|
+
| 洞 | 0.1.0 的行为 | 0.1.1 |
|
|
449
|
+
|---|---|---|
|
|
450
|
+
| 空文件 | `write_and_verify("report.md", "")` → `verified=True`,0 字节也算交付 | 无条件 `WriteRefused("empty")`;契约可加 `min_bytes` / `min_lines` |
|
|
451
|
+
| 假绿灯 | 写完再 `unlink`,`delivery_state()["complete"]` 仍是 `True`,而 `verify_all_delivered()` 是 `False` | 两处共用同一个 `_audit()`,逐条回盘上读;`written` / `missing` / `unverified` 三格互斥 |
|
|
452
|
+
| CRLF | 清单哈希按 Windows 检出后的 CRLF 字节算,换台机器整份失配 | `.gitattributes` 强制 `eol=lf`,清单标 `-text`;校验器按 LF 归一后比对 |
|
|
453
|
+
|
|
454
|
+
顺带两个:防误删锁的行数判据从「新内容 ≤2 行」改成新旧比例(填两行长注释绕不过去了);
|
|
455
|
+
`append=True` 不再能突破 8MB 上限(上限按**写完之后**的大小算)。
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## 0.1.2:四个洞 + 一个逃生口
|
|
460
|
+
|
|
461
|
+
0.1.1 之后又被盲测了一轮,四个都是真的。没有加新功能,只有判据和顺序:
|
|
462
|
+
|
|
463
|
+
| 洞 | 0.1.1 的行为 | 0.1.2 |
|
|
464
|
+
|---|---|---|
|
|
465
|
+
| 零宽字符 | `write_and_verify("report.md", "\u200b")` 过 `strip()` → 拿到 `verified=True`,盘上是个看不见的文件 | 用 `unicodedata.category()` 逐字符判可见性:`Cc/Cf/Cs/Co/Cn/Zs/Zl/Zp/Mn/Me` 全算不可见 → `WriteRefused("empty")` |
|
|
466
|
+
| 报错顺序 | 错名字 + 空内容,先挨「内容为空」,补完再来一次才发现名字也错 | 交付契约提到内容检查之前,一次调用只暴露一个问题,省一轮 Token |
|
|
467
|
+
| 适配器下限 | `min_bytes` / `min_lines` 只能写死在代码里 | 读 `VAC_MIN_BYTES` / `VAC_MIN_LINES`,与 `VAC_DELIVERY` 同一条通道(import 时封印) |
|
|
468
|
+
| 误伤 | 11 行 CSV 原地改写成 1 行 JSON 一律被拦 | `Workspace(..., allow_shrink=True)` 由开发者主动关掉防误删锁(默认 `False`) |
|
|
469
|
+
|
|
470
|
+
零宽那条值得单独说一句:**判据要按语义写,不能按字符编码写。** `strip()` 回答的是
|
|
471
|
+
「这段文字两头有没有空白」,而我们要问的是「这份文件打开之后有没有东西」。两个问题
|
|
472
|
+
长得像,后者多出来的那一半(零宽空格、BOM、方向控制符、孤立组合符)就是洞。
|
|
473
|
+
|
|
474
|
+
`allow_shrink` 只关「防误删」这一道:空内容、交付契约、读回校验照样立着。
|
|
475
|
+
它是给部署方的,不是给模型的 —— 模型手里没有构造 `Workspace` 的参数。
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## 0.1.3:一个红叉 + 第一屏(代码一行没动)
|
|
480
|
+
|
|
481
|
+
0.1.2 之后没有改判据、没有加闸。这一版只修「别人能不能用上」:
|
|
482
|
+
|
|
483
|
+
| 事 | 0.1.2 | 0.1.3 |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| Windows CI | 九个 job 里三个永远红 | 绿。根因不是代码:测试里的子进程会往 stdout 打中文,而 en-US 的 Windows 控制台代码页是 cp1252,编不出来直接把进程打挂。workflow 顶层加 `PYTHONUTF8=1`,两处 `subprocess.run` 显式 `encoding="utf-8"` |
|
|
486
|
+
| 第一屏 | 24KB 中文长文,第一行是警告 | 英文一句 + 30 秒示例 + **边界写在门口**(只挡 `write_file` 一个动词) |
|
|
487
|
+
| 发现路径 | 无 description / topics,未发 PyPI | 元数据补齐,`publish.yml` 用 OIDC 发版,不存 token |
|
|
488
|
+
| 清单 | `MANIFEST.sha256` 只在提交者本地对得上 | 改文件必重算(这次就重算了两次) |
|
|
489
|
+
|
|
490
|
+
**判据没变的证据**:60 项测试在 UTF-8 和 cp1252 两种环境下都是全绿;
|
|
491
|
+
`examples/bench.py` 两种都跑得出来;四道闸的代码一个字没改。
|
|
492
|
+
|
|
493
|
+
这也算一次自我验收:一个专治「声称」的库,改了文档就得把清单重算、把 CI 跑绿,
|
|
494
|
+
否则它自己就在犯它要治的病。
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 开发
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
pip install -e ".[dev]"
|
|
502
|
+
pytest -q # 60 项,全离线
|
|
503
|
+
python tools/check_manifest.py # 校验 MANIFEST.sha256(CRLF 检出也认)
|
|
504
|
+
ruff check .
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
MIT License.
|