Codex vs Cursor:能力对照与脏工作树验收方法
先按官方资料比较两边的入口、权限和规则,再用同一份脏工作树练习验收改动。本文提供完整离线夹具,真实产品会话结果留待运行。
- 按 IDE、CLI 与云端入口分别比较
- 先保存 dirty repo 基线,再验收代理新增 diff
- 审批、沙箱与模型质量需要分开验证
1. 先确认自己准备在哪儿完成这次改动
我会先拿一个正在维护的项目来选工具:打开文件、解释问题、修改、看差异、跑测试,直到我愿意接下这份改动。这个过程里,切换窗口的次数、权限提示是否看得懂、旧代码有没有被顺手改掉,比首页上的功能数量更接近实际成本。
2026 年 10 月的产品入口已经有明显重叠。Codex 有 CLI、IDE 扩展与云端任务,IDE 扩展也支持 Cursor。Cursor 的 Agent 能搜索、改文件和运行命令,还有 CLI、Agents Window 与 Cloud Agents。把两者各缩成一个终端和一个补全框,会漏掉大部分需要比较的工作。Codex IDE 文档、Cursor Agent 文档列出了这些入口。
这篇文章核对公开能力,并提供一份可以重跑的审查任务。真实 Codex、Cursor 模型会话和产品界面均未运行。文中的七张图都是概念图,命令帮助与离线夹具的结果另列;我没有据此填写模型得分、速度排名或价格胜负。
查看图示文字说明
Codex 包含 CLI、IDE 扩展和 Cloud;Cursor 包含编辑器、CLI 和 Cloud,并提供 Agents Window。比较时须明确入口、模型、工作树与权限。
2. 编辑器里的上下文,终端里的可重复操作
Codex IDE 扩展能把打开的文件和选区带入请求,并在代码旁审查修改。常用编辑器已经顺手的人,可以先在原来的环境里试一次,再决定是否迁移。需要把同一项检查放进脚本时,Codex CLI 的交互会话与 exec 入口更容易留下命令和输出。Codex CLI 文档说明了本地仓库和非交互工作流。
Cursor 编辑器适合把文件、终端、Agent 对话放在同一个工作面。当前 Agents Window 又增加了跨工作区、差异视图、PR 与 worktree 管理。Cursor CLI 也支持交互、打印输出与会话恢复,所以脚本化任务并非只能交给 Codex。真正该记录的是自己选了哪个入口,以及它实际用了哪份工作树。Agents Window与Cursor CLI可分别核对。
本地代理仍然可能调用远端模型。涉及公司源码时,我会先检查组织的数据策略、已连接工具和可用权限。本文的小任务使用无秘密、无远端仓库的临时目录,尽量减少这些环境差异对结果的干扰。
同名模型也未必带来相同结果,工具实现、系统指令、上下文组织和推理设置都会影响过程。如果两边能选到相同模型,可以把它记作一项控制条件;如果选不到,就把结果归到当时的完整配置。一次修复恰好成功,不足以说明另一工具在更多项目里都落后。
3. 少一个确认框,可能多一项需要检查的权限
Codex CLI 的 sandbox 与 approval policy 分别控制执行边界和审批时机。workspace-write 配合 on-request 允许工作区内的读写与命令,越界或网络请求可能需要批准;read-only 适合先看代码。实际限制还受配置和组织策略影响。never 表示不询问,遇到边界失败会返回给代理,不能把它理解成无限权限。OpenAI 审批与安全说明给出了这些组合。
Cursor 当前 Run Modes 包含 Auto-review、Allowlist 和 Run Everything。Auto-review 会结合允许列表、可用的命令沙箱与分类器判断;分类器允许的越界重试可能在沙箱外执行。官方明确提醒分类器会误判。我会保留实际设置的截图或文字记录,遇到批准提示时记录动作与范围,避免把较少弹窗当成安全性结论。Cursor Run Modes是这项设置的直接依据。
Cursor 的安全概览仍有默认逐次批准的表述,Run Modes 页面更细地说明了自动执行路径。本文不替所有版本给出一个默认值。运行前到 Approvals & Execution 查看当前模式,并把 CLI 沙箱、MCP 允许列表和团队策略一并记录。
查看图示文字说明
上层为任务和规则,中层为人工批准或自动审查,下层为文件、网络、凭据和沙箱。每层分别核对,不把自然语言规则当技术隔离。
4. 规则与 Skills 可以复用,实际加载仍要核对
我会先写一份短的仓库约定,说明测试入口、禁止修改的文件和交付方式。Codex 读取 AGENTS.md,并支持目录层级及 override;Cursor 也支持根目录和子目录的 AGENTS.md,另有带作用范围的 .cursor/rules/*.mdc。两边的发现顺序和触发机制不完全一致,共用文件之后仍要检查代理是否真的读到了。Codex 指令发现、Cursor Rules都应以安装版本为准。
两边都支持以 SKILL.md 组织可复用工作流;当前文档都包含 .agents/skills 路径。迁移时先检查所引用的脚本、路径和工具是否存在。MCP 连接还涉及服务器权限与身份,配置文件能搬过去,不代表另一环境已经能访问同一个服务。这个比较任务不需要 MCP,暂时关闭额外工具更容易解释结果。Codex Skills、Cursor Skills、Codex MCP、Cursor MCP提供了各自的配置范围。
# AGENTS.md
Preserve existing staged, unstaged and untracked user changes.
Before editing, inspect status and both staged and unstaged diffs.
Run tests with: python3 -B -m unittest -v test_labels
Do not install, use network/MCP, stage, commit, push or reset.
Report changed paths, exact checks, exit codes and limitations.这些文本约定便于沟通,不能代替沙箱或操作系统权限。为免一次试用被全局指令影响,记录表也保留规则、Skills、MCP 和插件清单;基线夹具本身不预装任何扩展。
查看图示文字说明
仓库内可包含共同 AGENTS.md、.agents/skills、Cursor 的 .mdc 规则;MCP 按客户端主机和身份验证。本次夹具只传递任务文本,不连接 MCP。
5. 这次专门看脏工作树里的审查
干净仓库里的补丁很容易看懂。真实项目常常已经有自己的改动:一个标题进了暂存区,README 还在写,旁边放着未跟踪的笔记。代理修好函数以后,Git 里的总差异会同时包含这些内容。只看最终 diff,很容易把自己的劳动算给代理,或者漏掉它撤销的旧改动。
夹具选择一个很小的待办计数函数。原实现把 1 也写成 items,目标是让单数变成 item,0 和 2 保持复数。源码里另有已经暂存的 APP_TITLE 修改。这里的难点是交付边界:允许改函数,标题、README、笔记、HEAD 和 index 都要保留。
准备脚本会创建两个全新的目录,不覆盖已有路径。每份仓库旁边都有运行前快照,其中保存文件字节、提交和暂存区状态。两边各用一份;第一次运行后即使失败,也重新准备下一组目录。不要让第二个工具接着第一个工具留下的补丁工作。
真实项目用 worktree 分开任务很有帮助,但新工作树通常从提交开始,本地未提交的草稿还要另外处理。本例直接复制出两组相同的脏状态,避免把原先的修改遗漏在实验之外。先检查哪些内容进入副本,再给代理任务,比结束后猜测文件去了哪里省事。
python3 -B fixture/prepare.py /tmp/codex-cursor-trial-01
git -C /tmp/codex-cursor-trial-01/codex status --short
git -C /tmp/codex-cursor-trial-01/codex diff --cached
git -C /tmp/codex-cursor-trial-01/codex diff
# Run the same inspection in the cursor directory.
# A new trial must use a new, unused directory name.查看图示文字说明
源码标题为已暂存改动,README 为未暂存草稿,notes.txt 为未跟踪文件;三者保持。唯一允许的新变更是 pending_label 函数,HEAD 与 index 不变。
6. 把同一份任务交给两边
先打开仓库并保存缓冲区,再开始记录。未保存的编辑器文本可能尚未进入磁盘,外部 CLI 和 Git 不一定看到它。实验里使用两份已保存的相同文件,模型名、推理设置、客户端版本和执行模式由操作者填写,不能只写一个产品名字。
下面的任务文本可以直接粘贴。它给出输入边界、修改范围、验证命令与停止条件,保留了遇到权限障碍时的出口。第一次运行就按这一版执行;中途补充过的信息也记录下来,避免一边拿到更多指导。
Fix pending_label(count) in src/labels.py for non-negative integer input.
Expected: 0 -> "0 items pending"; 1 -> "1 item pending";
2 -> "2 items pending". Preserve the public function signature.
Read git status, the staged diff, the unstaged diff, and untracked files first.
The APP_TITLE staged edit, README draft and untracked notes.txt belong to me.
Preserve them byte-for-byte, and leave HEAD and the staging area unchanged.
Only change the pending_label function in src/labels.py.
Do not install dependencies, access the network, call MCP, stage, commit,
push, reset, clean, restore a checkpoint, or change other files.
Run: python3 -B -m unittest -v test_labels
If any action needs permission or cannot run, stop and explain the blocker.
After the repair, stop. Return the exact changed file list, the command,
exit code and actual test log. Mention the existing edits you preserved.
Do not call a test passed when you did not run it.这个任务不比较复杂重构能力。它用很少的业务代码,让人能逐行检查最终结果,并观察代理是否注意已有改动、是否乱动暂存区,以及是否把没跑过的测试写进完成报告。
开始前自己跑一次测试,确认单数用例失败,另外两项通过。基线若已经全绿,说明拿错目录或遗留了上轮修复,应停止这一轮。结束后再用同一命令验证。这样至少能分清测试本来就在失败、代理引入了新问题、以及测试实际上没有执行这几种情况。
7. 在 Codex 里执行并留下证据
终端路径从帮助检查开始。本文实际读取了 Codex CLI 0.159.2 的 help,没有启动模型任务。下列交互命令使用明确的目录和权限组合;进入会话后粘贴上面的任务。只批准当前任务需要且范围清楚的动作,额外联网、安装或扩大目录权限应先停下来核查。
codex --version
codex --help
codex -C /tmp/codex-cursor-trial-01/codex \
--sandbox workspace-write --ask-for-approval on-request
# Paste fixture/task.txt after the session opens.
# Stop after the final report; save transcript and permissions separately.如果习惯 IDE,在该副本中打开 Codex 侧栏,附上 src/labels.py 与测试文件,再粘贴同一任务。把选区和打开文件也写进记录。使用 Codex 扩展与 Cursor 自带 Agent 是两个不同会话,哪怕都放在 Cursor 编辑器里,也要分别保存身份和日志。
非交互场景可改用 exec 保存事件和最终回复,但这会改变人工介入方式,应单独做一轮。不要混用交互结果和无人值守结果来统计速度。OpenAI 非交互文档说明了输出与自动化入口。
codex exec --help
codex exec --sandbox workspace-write --json \
-o ../codex-final.txt - < /path/to/fixture/task.txt \
> ../codex-events.jsonl
# Replace the task path. This is a separate trial, not a second run
# on the already modified interactive workspace.8. 在 Cursor 里执行并检查落盘结果
在 Cursor 打开另一个副本,确认当前选择 Local,保存所有文件并查看 Source Control。打开 Agent 面板,记下模型与 Approvals & Execution 的设置,再粘贴同一任务。过程中记录批准了什么、有没有改过任务要求,以及测试日志出现在哪儿。
Cursor 的工作区文件修改会立即保存到磁盘,开启自动重载的项目可能在审查之前运行新代码。这份夹具没有开发服务器,实际项目则应先关闭不必要的自动执行入口。任务完成后,从 Source Control 查看 staged 与 unstaged 两组差异,再对照最初快照。Cursor Agent Security明确说明了落盘与自动重载风险。
若要比较终端体验,另开一轮 Cursor CLI,使用下列官方命令入口。当前环境没有安装 agent,因此这些参数只经过官方文档核对,没有本机执行结果。先在自己的安装版本检查帮助;不匹配时保留帮助输出并调整记录,别为了对齐文章直接覆盖配置。
agent --help
agent --mode=ask
# Read-only exploration; end this session before the repair trial.
agent --sandbox enabled
# Paste fixture/task.txt. Record /sandbox network settings first.
# Save the final reply and actual permission events outside the repo.如果要与上面的 exec 一样保留无人值守记录,Cursor 使用 -p / --print。先用 agent models(或 --list-models)列出当前账号可用模型,再以 --model 指定并记录实际模型;不把文档中的示例名称当成账号必然可用的型号。--output-format 适用于 print 模式:text 只留最终回复,json 在成功结束后输出单个对象,stream-json 按行输出事件。失败时要检查退出码与 stderr,不能仅凭有输出文件就算完成;stream-json 还要检查末尾是否有成功的 result。它与 Codex --json 的事件结构不同,不能共用同一个事件解析器。Cursor CLI 参数、输出格式与Codex 非交互模式可分别核对。
print 不是只读权限开关。Cursor 的 Headless 文档把脚本直接改文件示例写成 -p --force,并说明该例不加 --force 只提出修改;权限参考同时明确 print 可调用写入与 shell 工具,实际执行还由 allow、deny 和 --force 控制,deny 优先。--force / --yolo 会放行未明确拒绝的命令,不能为了绕过阻塞直接补上。下面按文档参数给出独立的 Ask 探索与事件留存示例,不计入修复成绩。Ask 和提示词不应被当作进程或文件访问隔离保证;真正要做无人值守修复,应先核对安装版本、写入范围和有效权限,遇到与任务约束冲突的动作就停止。本段于 2026 年 10 月 6 日复核官方文档,未执行 Cursor 模型任务,也未验证这些参数在具体安装版本中的组合行为。
# Separate exploration round in a fresh Cursor fixture copy.
agent --version
agent --help
agent models
# Choose an ID available to this account; do not copy a fixed model name.
MODEL='REPLACE_WITH_AVAILABLE_MODEL_ID'
if agent -p --mode=ask --sandbox enabled --model "$MODEL" \
--output-format stream-json \
"Explain the singular-label defect in src/labels.py. Do not edit files, run commands, use network or MCP." \
> ../cursor-events.jsonl 2> ../cursor-stderr.txt; then
rc=0
else
rc=$?
fi
printf '%s\n' "$rc" > ../cursor-exit-code.txt
# Inspect the exit code and terminal result event before treating the run as successful.9. 审查要同时看本次新增和仓库总差异
我会先问两个问题:代理这次新增了什么,最终准备提交的整体又包含什么。前者用运行前快照比较,后者用 Git 的暂存与未暂存差异看。普通 git diff 不包含未跟踪文件;只看一张编辑器里的差异面板也可能遗漏另一组状态。
Codex 提供未提交、基准分支和指定提交的代码审查。Cursor 提供 Agent Review,可以手动输入 /agent-review,Source Control 的入口还可比较相对主分支的本地变化。这些审查的范围不必然等于本次代理新增的改动,先把自己的旧改动告诉审查者。Cursor Agent Review也提醒了设置位置随版本变化。
git status --short
git diff --cached -- src/labels.py
git diff -- src/labels.py README.md
git ls-files --others --exclude-standard
codex review --help
codex review --uncommitted
# Cursor: type /agent-review in the agent window.
# Treat review findings as another input, then inspect the patch yourself.检查点适合回到先前状态,但恢复操作会动文件。在有用户旧改动的目录里,我不会为了撤销一个函数修复就恢复整个快照。Cursor 文档也把检查点与 Git 分开说明。需要重跑比较时,新建夹具副本更清楚。
查看图示文字说明
保存仓库外的运行前快照,与运行后磁盘工作树比较,得到本次新增差异;再独立审查暂存、未暂存、未跟踪内容及测试证据。
10. 恢复会话之前,再确认一次执行位置
长任务经常需要中断。Codex resume 可以选择或指定先前会话;Cursor CLI 的 agent ls、agent --resume 也能继续指定对话。多仓库并行时我更愿意填写明确的会话 ID,恢复后先看目录、分支、status 和当前权限,防止把新的要求发到另一项任务里。
codex resume --help
codex resume SESSION_ID
agent ls
agent --resume=CHAT_ID
# After resuming: confirm working directory, branch, git status,
# effective permissions, and whether any previous command is still running.会话能继续,不代表本地程序在合上电脑之后还会工作。当前 Codex Cloud 的任务有独立工作区,可在电脑休眠时继续;Cursor Cloud Agents 在隔离 VM 中工作,也不要求本机在线。云端要重新核对仓库版本、依赖、网络、密钥和连接服务,不能假定本地权限自然继承。Codex Cloud与Cursor Cloud Agents描述了各自的执行环境。
这次 dirty repo 比较固定在本地。云端常从仓库分支准备环境,本机未提交、未跟踪的内容如何带过去需要单独验证。后台执行也不会替人决定是否批准敏感动作或合并结果;停止、失败、等待批准都应留下明确状态。
查看图示文字说明
本地路径记录会话 ID、恢复历史、确认机器与权限后继续执行;云端另核仓库版本、依赖、网络和服务身份,不能默认带入未提交草稿。
11. 把结果写成下一次还能检查的记录
附录中的 fixture/gate.py 读取运行前快照与当前目录,检查 HEAD、index、文件清单、旧文件字节、普通文件的权限位和函数外的保护内容,再核对报告的 changed_files。它不会导入候选代码。这样即使测试声称通过,也能单独发现丢失笔记、重置标题或漏报新增文件。
只比较文件字节会漏掉权限变化。本轮离线复现中,正确修复函数后,再把 README.md 从 0644 改成 0744,旧版 gate 仍然通过;git diff --summary 却能看到该文件的可执行位变化。新版快照用 stat.S_IMODE 保存每个普通文件的权限位,任何权限位变化都会让 gate 失败,并列入 mode_changed_files 与 changed_files。即使变的是允许修改的 src/labels.py,也不能借函数修复顺带改权限。
升级夹具后要重新准备一组未使用的试验目录;旧快照没有 modes 时,gate 会明确拒绝,不能用运行后的文件补填运行前基线。权限位不等于完整文件系统元数据:这里没有验收目录权限、所有者、ACL 或扩展属性,也没有验证跨操作系统复制的等价性。权限变化回归只在 POSIX 环境执行,其他环境会明确跳过,不应把跳过计作通过。
这道检查只覆盖本例的结构约束。函数体内的实现仍需人工审查与实际测试;测试日志由操作者保存,评估器不替日志背书。附录自测保留了手写参考修复,用来证明检查路径可用,不能记作任一模型的成绩。
{
"tool": "fill in product, version, model, mode and session ID",
"origin": "actual_run_or_manual_reference",
"changed_files": ["src/labels.py"],
"tests": [],
"permission_events": [],
"review_notes": "Record the reviewed diff and remaining uncertainty"
}python3 -B fixture/gate.py \
/tmp/codex-cursor-trial-01/codex.before.json \
/tmp/codex-cursor-trial-01/codex \
/tmp/codex-cursor-trial-01/codex.result.json
# Repeat with cursor.before.json, cursor and cursor.result.json.
# In each reviewed repository, independently rerun:
python3 -B -m unittest -v test_labels| 记录项 | 需要保存的证据 |
|---|---|
| 环境与上下文 | 版本、模型、入口、规则、插件、打开文件、基线快照 |
| 权限与介入 | 沙箱、网络、审批模式、每次批准和人工补充 |
| 修改与验证 | 新增 diff、总 diff、gate 输出、真实测试命令与日志 |
| 结束状态 | 完成、失败、阻塞或取消;会话 ID;实际耗时与人工审查耗时 |
选型时先看哪些记录反复影响工作。如果主要成本是找文件和审查差异,就多试一次 IDE 流程;若重复任务经常要接脚本,就单独测 CLI;要离开电脑后继续,再准备云端轮次。用同一类真实任务积累几次记录,才有理由讨论团队该留下哪套组合。
查看图示文字说明
已核官方能力和文档,已运行本机 Codex 帮助与离线 gate,手写参考经过三项行为测试。真实模型、UI、后台及恢复均未运行。
12. 本稿实际验收到了哪里
本稿原有记录检查了官方能力说明,运行了 Codex CLI 0.159.2 的版本和帮助命令。离线 gate 的 13 项自测通过,手写参考修复通过三项行为测试,原始单数缺陷可复现。全部来自本文离线夹具,未调用 Codex 或 Cursor 模型生成补丁。
没有执行 Cursor CLI、产品 UI、真实后台任务、MCP 接入或恢复会话,也没有验证具体套餐、账单、模型质量和云端权限继承。页面里的操作步骤供读者在已授权的环境复现;核对日期是 2026 年 10 月 5 日,遇到界面和参数变化应重新查帮助。
2026 年 10 月 20 日只复跑离线教学夹具:Linux、Python 3.12.14、Git 2.52.0 下,原始单数测试仍失败,人工参考修复通过三项行为测试;修正后的 gate 共 18 项自测全部通过、无跳过,新增 README、源码与未跟踪笔记的权限位回归,以及缺失或无效模式快照检查。完整命令行验收中,纯函数修复退出 0,附带 README 权限位修改退出 1,已有试验目录仍拒绝覆盖。本轮没有重跑产品 CLI、模型或 UI,不补填真实产品成绩。
13. 官方来源
14. 附录:离线夹具完整源码
本页已列出离线夹具所需的三个完整文件,无需另外下载资料包。新建一个资料目录,在其中新建 fixture 子目录,再把下面的源码分别保存为 fixture/prepare.py、fixture/gate.py 和 fixture/test_gate.py。从资料目录运行正文里的 fixture/prepare.py 与 fixture/gate.py 命令;自测命令为 python3 -B -m unittest discover -s fixture -p test_gate.py -v。test_gate.py 内含人工参考修复,用于检查 gate,不是任一产品的输出。运行记录可复制正文里的 JSON 模板,填写实际结果。
源码已包含权限位快照;请用同版 prepare.py 与 gate.py 新建一轮,不复用旧版 before.json。脚本只适用于本文新建的小型仓库;它不会隔离恶意代码,也不应拿来扫描含秘密的大型工作目录。
fixture/prepare.py · 完整源码
"""Create two new disposable repositories; never overwrite a directory."""
import json
import os
from pathlib import Path
import subprocess
import sys
from gate import snapshot
BASE = '''APP_TITLE = "Tasks"
def pending_label(count):
return f"{count} items pending"
'''
TESTS = '''import unittest
from src.labels import pending_label
class Labels(unittest.TestCase):
def test_zero(self):
self.assertEqual(pending_label(0), "0 items pending")
def test_one(self):
self.assertEqual(pending_label(1), "1 item pending")
def test_many(self):
self.assertEqual(pending_label(2), "2 items pending")
'''
def prepare(root):
root = Path(root).resolve()
root.mkdir(parents=True, exist_ok=False)
(root / "src").mkdir()
(root / "src/labels.py").write_text(BASE)
(root / "test_labels.py").write_text(TESTS)
(root / "README.md").write_text("# Label fixture\n")
(root / ".gitignore").write_text("__pycache__/\n*.pyc\n")
env = dict(os.environ, GIT_CONFIG_GLOBAL="/dev/null",
GIT_CONFIG_NOSYSTEM="1", GIT_TERMINAL_PROMPT="0",
GIT_AUTHOR_DATE="2026-10-05T00:00:00Z",
GIT_COMMITTER_DATE="2026-10-05T00:00:00Z")
def git(*args):
subprocess.run(["git", "-c", "core.hooksPath=/dev/null", *args],
cwd=root, env=env, check=True, capture_output=True)
git("init", "-b", "main")
git("add", ".")
git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid",
"commit", "-m", "baseline")
(root / "src/labels.py").write_text(BASE.replace('"Tasks"', '"My tasks"'))
git("add", "src/labels.py")
(root / "README.md").write_text("# Label fixture\n\nUser draft: keep me.\n")
(root / "notes.txt").write_text("User note: not tracked; keep these bytes.\n")
return snapshot(root)
if __name__ == "__main__":
parent = Path(sys.argv[1]).resolve()
parent.mkdir(parents=True, exist_ok=False)
for label in ("codex", "cursor"):
before = prepare(parent / label)
(parent / f"{label}.before.json").write_text(
json.dumps(before, indent=2))
print(f"Created independent repositories in {parent}")fixture/gate.py · 完整源码
"""Offline diff/result gate. It never imports or executes candidate code."""
import ast
import base64
import hashlib
import json
from pathlib import Path
import stat
import subprocess
import sys
ALLOWED = "src/labels.py"
def snapshot(root):
root = Path(root).resolve()
def git(*args):
return subprocess.check_output(
["git", "-C", str(root), *args]).decode()
files, modes = {}, {}
for p in sorted(root.rglob("*")):
rel = p.relative_to(root)
if rel.parts[0] == ".git":
continue
if p.is_symlink():
raise ValueError(f"Symlink unsupported: {rel}")
if p.is_file():
files[rel.as_posix()] = base64.b64encode(p.read_bytes()).decode()
modes[rel.as_posix()] = stat.S_IMODE(p.stat().st_mode)
return {"head": git("rev-parse", "HEAD"),
"index": git("ls-files", "--stage", "-z"),
"files": files, "modes": modes}
def evaluate(before, after, result):
reasons = []
if not isinstance(result, dict):
reasons.append("Result must be a JSON object")
result = {}
if before["head"] != after["head"]:
reasons.append("HEAD changed")
if before["index"] != after["index"]:
reasons.append("Index changed")
old, new = before["files"], after["files"]
old_modes, new_modes = before.get("modes"), after.get("modes")
modes_valid = all(
isinstance(modes, dict) and set(modes) == set(files)
and all(type(mode) is int and 0 <= mode <= 0o7777
for mode in modes.values())
for modes, files in ((old_modes, old), (new_modes, new)))
mode_changed = []
if not modes_valid:
reasons.append("Missing or invalid mode snapshot; prepare a fresh trial")
else:
mode_changed = sorted(p for p in set(old_modes) | set(new_modes)
if old_modes.get(p) != new_modes.get(p))
if mode_changed:
reasons.append("File permission bits changed")
changed = sorted(
{p for p in set(old) | set(new) if old.get(p) != new.get(p)}
| set(mode_changed))
if changed != [ALLOWED]:
reasons.append("Unexpected changed paths or no patch")
try:
source = base64.b64decode(new[ALLOWED], validate=True).decode()
previous = base64.b64decode(old[ALLOWED], validate=True).decode()
tree, prior = ast.parse(source), ast.parse(previous)
if (len(tree.body) != 2 or not isinstance(tree.body[1], ast.FunctionDef)
or tree.body[1].name != "pending_label"
or ast.dump(tree.body[0]) != ast.dump(prior.body[0])
or ast.dump(tree.body[1].args) != ast.dump(prior.body[1].args)
or tree.body[1].decorator_list):
reasons.append("Protected title or function boundary changed")
prefix = previous.split("def pending_label", 1)[0]
if not source.startswith(prefix):
reasons.append("Protected source prefix changed")
if (len(tree.body) == 2 and isinstance(tree.body[1], ast.FunctionDef)
and source.splitlines()[tree.body[1].lineno - 1]
!= previous.splitlines()[prior.body[1].lineno - 1]):
reasons.append("Public function declaration changed")
except (KeyError, SyntaxError, ValueError, UnicodeError):
reasons.append("Missing or malformed source")
if result.get("changed_files") != changed:
reasons.append("Result changed_files disagrees with snapshot")
if not isinstance(result.get("tests"), list):
reasons.append("Result tests must be an evidence list")
test_items = result.get("tests", [])
for item in test_items if isinstance(test_items, list) else []:
if (not isinstance(item, dict) or not isinstance(item.get("command"), str)
or type(item.get("exit_code")) is not int
or not isinstance(item.get("log"), str)):
reasons.append("Malformed test evidence entry")
return {"gate_pass": not reasons, "reasons": reasons,
"changed_files": changed,
"mode_changed_files": mode_changed,
"candidate_sha256": hashlib.sha256(
base64.b64decode(new.get(ALLOWED, ""))).hexdigest(),
"test_claims_verified": False,
"scope": "Structural diff/result checks only; not a safety sandbox"}
if __name__ == "__main__":
before = json.loads(Path(sys.argv[1]).read_text())
after = snapshot(sys.argv[2])
result = json.loads(Path(sys.argv[3]).read_text())
report = evaluate(before, after, result)
print(json.dumps(report, indent=2))
raise SystemExit(0 if report["gate_pass"] else 1)fixture/test_gate.py · 完整源码
import copy
import unittest
from pathlib import Path
import tempfile
import base64
import os
import stat
from gate import evaluate, snapshot, ALLOWED
from prepare import prepare
class GateTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name) / "repo"
self.before = prepare(self.root)
p = self.root / ALLOWED
p.write_text(p.read_text().replace(
'return f"{count} items pending"',
'unit = "item" if count == 1 else "items"\n'
' return f"{count} {unit} pending"'))
self.after = snapshot(self.root)
self.result = {"changed_files": [ALLOWED], "tests": []}
def report(self):
return evaluate(self.before, self.after, self.result)
def test_manual_reference(self):
self.assertTrue(self.report()["gate_pass"])
self.assertFalse(self.report()["test_claims_verified"])
def test_index_change(self):
self.after["index"] += "modified"
self.assertFalse(self.report()["gate_pass"])
def test_head_change(self):
self.after["head"] = "another commit"
self.assertFalse(self.report()["gate_pass"])
def test_untracked_deletion(self):
del self.after["files"]["notes.txt"]
del self.after["modes"]["notes.txt"]
self.assertFalse(self.report()["gate_pass"])
def test_unrelated_change(self):
self.after["files"]["README.md"] = ""
self.assertFalse(self.report()["gate_pass"])
def test_title_reverted(self):
data = base64.b64decode(self.after["files"][ALLOWED])
self.after["files"][ALLOWED] = base64.b64encode(
data.replace(b"My tasks", b"Tasks")).decode()
self.assertFalse(self.report()["gate_pass"])
def test_inaccurate_result(self):
self.result["changed_files"] = []
self.assertFalse(self.report()["gate_pass"])
def test_added_file(self):
self.after["files"]["extra.txt"] = ""
self.after["modes"]["extra.txt"] = 0o644
self.assertFalse(self.report()["gate_pass"])
def test_no_change(self):
self.after = copy.deepcopy(self.before)
self.assertFalse(self.report()["gate_pass"])
def test_invalid_python(self):
self.after["files"][ALLOWED] = base64.b64encode(b"def broken").decode()
self.assertFalse(self.report()["gate_pass"])
def test_non_object_result(self):
for result in (None, [], "wrong", 1, True):
with self.subTest(result=result):
self.result = result
self.assertFalse(self.report()["gate_pass"])
def test_return_annotation_change(self):
data = base64.b64decode(self.after["files"][ALLOWED])
self.after["files"][ALLOWED] = base64.b64encode(data.replace(
b"def pending_label(count):", b"def pending_label(count) -> str:")
).decode()
self.assertFalse(self.report()["gate_pass"])
def test_missing_function(self):
for source in (b"", b"APP_TITLE = 'My tasks'\n"):
with self.subTest(source=source):
self.after["files"][ALLOWED] = base64.b64encode(source).decode()
self.assertFalse(self.report()["gate_pass"])
def assert_mode_change_rejected(self, path):
p = self.root / path
p.chmod(stat.S_IMODE(p.stat().st_mode) ^ stat.S_IXUSR)
self.after = snapshot(self.root)
report = self.report()
self.assertFalse(report["gate_pass"])
self.assertEqual(report["mode_changed_files"], [path])
self.assertIn(path, report["changed_files"])
self.assertIn("File permission bits changed", report["reasons"])
@unittest.skipUnless(os.name == "posix", "Requires POSIX permission bits")
def test_unrelated_mode_change(self):
self.assert_mode_change_rejected("README.md")
@unittest.skipUnless(os.name == "posix", "Requires POSIX permission bits")
def test_allowed_file_mode_change(self):
self.assert_mode_change_rejected(ALLOWED)
@unittest.skipUnless(os.name == "posix", "Requires POSIX permission bits")
def test_untracked_mode_change(self):
self.assert_mode_change_rejected("notes.txt")
def test_missing_mode_snapshot(self):
for target in ("before", "after"):
with self.subTest(target=target):
saved = getattr(self, target).pop("modes")
self.assertFalse(self.report()["gate_pass"])
getattr(self, target)["modes"] = saved
def test_invalid_mode_snapshot(self):
saved = self.after["modes"]
variants = (None, [], {}, {**saved, "extra.txt": 0o644},
{**saved, ALLOWED: True}, {**saved, ALLOWED: -1},
{**saved, ALLOWED: 0o10000},
{**saved, ALLOWED: "0644"})
for modes in variants:
with self.subTest(modes=modes):
self.after["modes"] = modes
self.assertFalse(self.report()["gate_pass"])
self.after["modes"] = saved
if __name__ == "__main__":
unittest.main()Codex 能在 Cursor 里用吗?
官方 IDE 文档把 Cursor 列为兼容编辑器。Codex 扩展和 Cursor 自带 Agent 仍是不同会话,分别记录模型、权限与任务结果。
Cursor 的 CLI 和云端任务也能改整个仓库吗?
官方文档包含终端代理与 Cloud Agents,可使用文件和执行工具。具体范围取决于当前环境、模式、组织设置与授予的权限。
这里证明了哪一个模型更强吗?
没有。这里验证了公开能力和离线验收方法,真实模型运行记录尚未填写。
gate 通过后可以直接合并吗?
还要人工检查实现并运行独立测试。gate 只检查本例的差异结构与报告一致性,不验证测试声明,也不是安全沙箱。