Repository navigation
0.4.0:零依赖、Windows 兼容层与 AgentSandbox - #6
Merged
Merged
Conversation
added 15 commits
September 28, 2026 17:02
此前包内直接使用 fcntl / os.pread / os.pwrite / os.getuid / 目录 fsync,
其中 infrastructure/checkpoint.py 是顶层 import fcntl —— 在 Windows 上连
import pyvdisk 都会失败,不只是功能缺失。
新增 pyvdisk/compat.py 作为唯一的平台原语入口:
- 定位读写:POSIX 用 os.pread/os.pwrite,Windows 退化为互斥锁保护的
lseek+read/write(全局 RLock,保证定位与读写不可分割);
- 文件锁:POSIX 用 fcntl.flock,Windows 用 msvcrt.locking 锁整个文件区间。
刻意不用 LK_LOCK:它在 Windows 上有约 10 秒超时后会直接抛错,而 flock
是无限等待;这里用 LK_NBLCK 轮询自己实现 blocking/timeout 语义;
- 目录 fsync:Windows 无法把目录当文件打开,返回 False 表示"未执行"而非成功;
- uid/gid:Windows 返回 0,另提供 uid_or/gid_or 以免把 root(0) 当成"未设置";
- can_symlink():Windows 需要开发者模式,探测结果供调用方决定是否降级。
四个使用点全部改走 compat:disk.py、infrastructure/checkpoint.py、
vscript/host.py、fuse_mount.py。
tests/test_compat.py 覆盖两层:
- 真实平台上的行为(定位读写、锁竞争、超时放弃、解锁重取、目录 fsync);
- 用 compat.use_implementation("windows") + 替身 msvcrt 在 Linux 上真正执行
模拟实现,包括锁竞争与 timeout 分支,并在模拟下跑通 VFS/DataDisk/事务/
检查点的完整往返,以及与原生实现的逐字节对照。
注意:替身锁是进程内的,无法证明两个**进程**之间互斥,这需要 Windows runner。
最后一条守卫测试固定住"只有 compat.py 可以直接触碰平台原语"这一约束——
Windows 缺口正是从这条约束缺失开始的。
dependencies 由 ["hnswlib>=0.8.0"] 改为 []:pip install pyvdisk 不再需要 编译任何东西,这也是"单文件快速上手"的前提。hnswlib 仍是唯一的第三方 import,把它变成可选之后,核心路径回到纯标准库。 新增 pyvdisk/vector_index.py:扁平(精确)索引后端,接口对齐 hnswlib 的 使用形状(init_index/set_ef/add_items/knn_query/save_index/load_index), 距离约定也一致(cosine = 1 - 相似度,l2 = 平方欧氏距离,ip = 1 - 点积), 因此两种后端可以直接互换,不需要在调用点分支。 - VectorDisk._index_backend() 装了 hnswlib 就用它,否则回退内置后端; - 索引文件带 8 字节魔数 PVFLAT01,读方据此判断写入方。文件名保持 index.hnsw 不变(磁盘格式兼容),由魔数而不是文件名决定如何解析; - 两种后端互相可读:hnswlib 写的盘在无 hnswlib 环境下用 flat 后端精确 查询(线性扫描,结果正确,优于拒绝打开),反向亦然; - 重建索引改为一次性序列化后经 VFS 原子改名发布,去掉了临时文件。 代价要说清楚:flat 后端查询是 O(n) 且索引常驻内存,只适合中小集合。 hnswlib 与 fusepy 降级为可选 extra(pyvdisk[vector] / pyvdisk[fuse])。 CI 相应调整:版本矩阵装 [dev,vector] 保住 hnswlib 路径的覆盖(否则它会 悄悄不再被测试),另加一个 no-dependencies job 只装 [dev],断言 hnswlib/ fuse/numpy 都不可导入且核心仍能跑通全套用例。 tests/test_no_dependency_path.py 在 import 层面屏蔽 hnswlib,验证无加速器 时集合仍可增删查、索引可持久化、两种后端结果一致,以及 examples/ quickstart.py 在无依赖环境下真的能跑完。
把 DataDisk 包成一个可以交给模型的工作区:工具 schema、调用分发、路径 约束、审计一次到位,框架侧只需要 tools() + dispatch() 两个入口。 - create() / open():新建或续用已有镜像; - tools(style=):openai / anthropic / mcp 三种 schema,6 个内置工具 (write_file、read_file、list_files、make_directory、delete_file、 run_script); - dispatch() 永远返回字符串(失败也是字符串),可直接塞回消息列表; call() 返回结构化 ToolResult 供程序使用; - 约束:read_only 时在 capability 层拒绝写(不只是不暴露工具), allow_delete 可单独关掉删除; - 路径空间是镜像不是宿主:.. 直接拒绝而不解析(拒绝行为要能在审计里看到), C:\Windows\x 映射为镜像内 /C:/Windows/x,/.system、/.vectors、/.logs 及其清单文件对读写与列举一律不可见; - 审计:每次调用(含被拒绝的调用)写入哈希链流 agent-audit,记录工具名、 参数、状态、错误类型与能力摘要,可 query/verify/retain; - run_script:盘内 VScript,默认禁止宿主读写,脚本超时与输出/写入字节数 均受限,避免脚本刷爆模型上下文。 修掉两处首次实现里的真实缺陷: 1. 能力授权名写成了沙箱名,而 _check 是按 fs/vector/log/checkpoint 这些 **命名空间**名匹配的,等于所有 fs 操作都会被拒。改为 "*"。 2. 脚本入口原本直接挂载 disk.fs,于是 fs.listdir(sandbox, "/") 能看到 /.system、/.logs,与 Python 侧 list_files 的规则不一致 —— 同一个镜像 两个入口两套规则。新增 _SandboxFS 让脚本侧走同一份路径规则:隐藏项在 读取/写入时拒绝、在 listdir/listdir_with_stat 时过滤、在 walk/glob 时 连目录本身一起剪掉(否则 fs.glob 仍会吐出 /.system)。两种入口现在共用 一个工作区,也共用同一套盲区。 tests/test_agent_sandbox.py 覆盖上述承诺,包括两个入口的根目录一致性、 路径逃逸与容器内部路径的拒绝、只读/禁删模式、审计链校验与篡改检测、 脚本的宿主隔离与输出上限。
examples/quickstart.py:单文件、仅标准库、Windows/Linux 通用,自底向上跑通 块设备与文件系统、事务与检查点、向量检索(有/无 hnswlib 两种后端)、结构化 日志、VScript、AgentSandbox。刻意不依赖 pyvdisk 已安装(未安装时把仓库根加进 sys.path),保证"下载即可运行"。 examples/agent_tools.py:一个可运行的 agent loop,内置脚本化"模型", 不需要 API key 与网络,因此可以进 CI。循环本身与对接真实框架时完全一致, 只把 scripted_model 换成真实客户端即可;示例里包含一次被拒绝的越权读取, 说明拒绝会像普通工具结果一样回灌给模型。 README: - 快速开始补上零依赖说明、quickstart 一行命令、可选 extra 的安装方式; - 新增"对接 Agent 框架(10 行)"小节; - 架构图 Vector 一格改为 "HNSW / flat memory"; - 核心能力补"零外部依赖"一条; - 回归数字 391 → 444(不装 hnswlib 时 440 passed / 4 skipped),并说明 跳过的是对照类用例而非核心路径。 CHANGELOG 记录这三块改动,含 flat 索引的代价(O(n) 查询)与两种后端的互读性。
变更 2026-09-24-zero-dep-windows-agentsandbox(已 verified,revision.after = 2c9009b): 新增结构: - pyvdisk.platform(平台适配层,4 个模块):实现选择、定位读写、跨进程文件锁、 持久化与身份。设计上它是"唯一直接触碰平台原语的地方",其余模块经它中转。 - pyvdisk.agent(Agent 工作区,5 个模块):门面、路径约束、工具面、盘内脚本、 工具调用审计。 - pyvdisk.storage.vector.flat-index:无依赖的精确索引后端。 - pyvdisk.examples(2 个模块)与 3 个新测试模块(compat / agent-sandbox / no-dependency)。 同步既有结构:block 的 image/access/io 补上经 platform 的调用箭头;checkpoint、 fuse.meta、vscript.host.guard 同样挂到 platform;vector 的 open/search 更新为 "后端由文件魔数决定、缺 hnswlib 自动回退";api 补上 AgentSandbox/compat/ FlatIndex 三个导出;tests 与 delivery/docs 的计数与介绍对齐当前状态。 渲染数据:根、tests、agent、platform、examples、storage.vector 各层已重排, 其中根层新增"对外接口与 Agent 入口"分组。新模块的 API 键改为 pyvdisk.compat.* 形式,避免与既有的 pyvdisk.api 键空间撞车。 结果:290 模块 / 844 API / 132 箭头 / 41 层渲染数据,0 error,15 warning (多为既有的 leaf-too-coarse 提示,非阻塞)。
上一版的 10 行 Demo 有三个会让直接复制的人卡住的缺陷: 1. 用了从未定义的 messages 与 json:示例不导入 json,消息列表凭空出现; 2. json.loads(call.function.arguments) 写成了取 call.arguments —— 参数在 call.function 上,照抄必抛 AttributeError; 3. messages += [reply.choices[0].message, ...] 只处理了 tool_calls[0], 模型一轮请求多个工具时会静默丢掉其余调用。 重写为逐条处理 tool_calls、各自用 call.id 回填 tool_call_id,并把 assistant 消息本身也追加进 messages(不追加的话模型看不到自己上一轮请求过什么)。 另外把三个易踩点直接写进正文,而不是只藏在可运行示例里。 examples/agent_tools.py 同步改成与 README 完全一致的循环形状(用 _Message/ _ToolCall 复刻客户端的返回结构),并新增一轮"两个工具同时请求"的用例, 让列表分支真的被执行到。首次运行时它确实暴露了一个真实缺陷:脚本入口挂载 的是原始盘,fs.listdir(sandbox, "/") 能看到 /.system、/.logs(Python 侧 list_files 看不到)。这在前一提交里已由 _SandboxFS 修复,此处同时是所有 工具面测试的回归点。 tests/test_agent_sandbox.py 增加两条守卫,防止 Demo 再次漂移:一条用 ast 解析 README 里的代码块,断言它语法正确、定义了用到的名字、且用的是真实的 API 形状;另一条按 Demo 的写法构造 call 对象,实际跑一遍内部两行。
变更 2026-09-24-readme-agent-demo-fix(verified,revision.after = f163400): docs.readme/docs 更新指纹;examples.agent-tools 更新脚本化模型说明(含一轮 多工具调用);tests.agent-sandbox 补上 README 示例守卫的说明;tests 计数 对齐 446。290 模块 / 844 API / 132 箭头,0 error。
在干净的克隆里用系统 Python 复核 Windows 兼容层时发现:强制切到 windows 模拟后, describe() 仍然报告 locking=fcntl.flock、directory_fsync=True、uid_gid=True —— 它描述的是宿主平台,而不是当前生效的实现。拿它做诊断或测试断言的人会得到 恰好相反的结论,而这恰恰是本次改动的核心承诺("Windows 上真的有锁、真的不做 目录 fsync")。 改为按 implementation() 分支:模拟生效时报告 lseek+read/write、msvcrt.locking、 directory_fsync=False、uid_gid=False。 原测试漏掉了这一点,因为它只断言了原生实现的分支;补一条在 windows 模拟下 断言 describe() 全部字段的用例。
变更 2026-09-24-compat-describe-live-impl(verified,revision.after = 3cb4787): platform 四个模块(dispatch/positional/locking/durability)与 tests.compat、 docs.readme 的指纹已刷新;platform.dispatch 的介绍明确 describe() 描述的是 生效实现而非宿主平台。290 模块 / 844 API / 132 箭头,0 error。
… can pass The no-dependencies job installs .[dev] and then asserts that neither hnswlib, fuse nor numpy is importable. Having hnswlib in the dev extra made that job fail on CI with 'installed: hnswlib,numpy' -- the job was detecting the accelerator it had just installed itself, not a real leak. The accelerator already has its own 'vector' extra, and the test matrix installs .[dev,vector], so the hnswlib code path stays covered. Splitting the two is what lets the no-dependencies job actually prove the claim it exists to prove.
…al runs The verification section claimed "447 passed(本地 3.12;CI 覆盖 ...)" and the badge said 447 passed with no qualifier. Both invited the same misreading: that CI had produced 447. It had not -- this branch's numbers had only ever been measured locally. The branch has now run on CI (all five jobs green), so the numbers can state where they come from. Read out of the job logs: tests (3.9 .. 3.12) -> 447 passed no-dependencies -> 443 passed, 4 skipped Also say which four cases skip and why (they need hnswlib and the built-in flat index present at once to compare the two backends), rather than leaving the reader to guess, and add a zero-dependency badge since that is now a property the CI actually enforces.
_retry() treated every OSError as "someone else holds the lock" and polled at POLL_SECONDS. With blocking=True and no timeout that loop never ends: a stale or bad descriptor (EBADF) or a dead filesystem now spins at 100 Hz forever instead of failing fast. Restrict the retry to errnos that actually mean contention or interruption (EAGAIN/EWOULDBLOCK, EACCES, EINTR, EDEADLOCK) and re-raise the rest from both the blocking and the deadline loop. A non-blocking probe now reports contended (False) only for real contention; any other error raises. Two regression tests pin this: one through the Windows emulation (closed fd + timeout must raise well before the deadline, not return False), one against the native POSIX path (non-blocking probe on a closed fd raises instead of reporting contention).
Line 9 lost its identifier to whatever pipeline produced the file; restore the intended wording so a reader knows what to swap scripted_model for.
Drop the [Unreleased] heading onto a dated version. 0.4.0 is the first release carrying the zero-dependency flat index, the compat layer and AgentSandbox; the changelog section was already written for exactly that set.
The 3.4 MB build artifact (272 module pages, renders, tree.json, receipt) has been living inside the code tree, so every docs sync turned routine commits into noise and the tree stopped reading as code. The artifact now lives on its own orphan branch, normify, which holds normify-pyvdisk/ at the same path and nothing else; this branch is the archive location, regenerated only when the map itself is rebuilt. The README links (interactive htmlpreview URL and the artifact table) point there now, so the map stays one click away from the README without occupying the repository's file tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
内容
dev/agentsandbox→master:零依赖 + Windows 兼容层收敛 + AgentSandbox。11 个提交的完整叙事已在 CHANGELOG[0.4.0]一节;本 PR 在其上追加 4 个合并前提交:_retry只对锁冲突 errno(EAGAIN/EACCES/EINTR/EDEADLOCK)重试,其余 OSError 立即抛出——此前坏描述符会以 100 Hz 永久空转。附两个回归测试(Windows 模拟路径 + POSIX 原生路径)。examples/agent_tools.py的[OI]→ OpenAI。__version__、CHANGELOG 日期)。normify,代码树不再含产物;README 的交互图谱链接与产物表改为指向该分支,索引不丢。验证
pylint -E0 error合并后跟进(另开 issue)
windows-latest跑零依赖 job(PR fix(windows): 让镜像层、宿主锁与 CLI 在 Windows 上可用 #5 与本分支共同的债:CI 全绿 ≠ Windows 绿)_SandboxFS广告的方法面大于 ScopedFileNamespace 实现面(死面返回裸 AttributeError)