Skip to content
工程案例研究
待周评
点了取消,究竟取消了哪一步?一次 Agent 执行边界实验
开源工程观察 · 受控实验

点了取消,究竟取消了哪一步?一次 Agent 执行边界实验

发出取消请求、取消被接受、动作被阻止,是三个不同的事实。我们用真实审批服务和会话适配器做受控对照,发现有些看似越界的执行其实来自对接口语义的误读;真正还需要验证的,是授权答复交出之后、动作发生之前的等待窗口。

2026-09-06English →

查看题图原图

点了取消,究竟取消了哪一步?一次 Agent 执行边界实验

让 AI 助手修改文件,最让人不安的时刻之一,是你已经改变主意,它却还显示“正在处理”。

你点了取消。接下来,它应当不再开始新动作,还是立即终止正在执行的命令?如果文件已经写了一半,是停下来、写完再停,还是恢复原状?

这些不是同一个承诺。一个“取消”按钮可以表达明确的用户意愿,却不能单凭点击本身证明执行端已经接受请求,更不能证明外部动作尚未发生。

我们最近做的一组实验,恰好遇到了这种容易下错结论的现场:执行器在等待,我们发出取消,随后文件仍然写入成功。只看前后顺序,很容易写成“取消后仍执行”。但检查回执后,结论必须改变——那次取消请求根本没有被接受。

这篇文章要讲的,不是一个已经修好的取消漏洞,而是如何把“我要求它停”追踪到“哪一步确实停了”。

为此,我们分别检查取消待审批请求、结束会话、动作执行前中止三个边界。它们不是同一个接口,也不承诺相同结果:下文先看审批服务,再看会话适配器,动作前的额外中止检查则只作为研究对照,不是一次界面按钮的端到端测试。

1. 一个值得追问的窗口:检查之后,还要等多久?

OpenHands 是面向软件开发任务的开源 Agent 项目,其 SDK 提供工具执行等基础能力。这里的 Agent,指能够调用工具采取动作的 AI 助手,而不只是回答问题的聊天模型。

OpenHands SDK 的 #4866 提案讨论了一段具体顺序:工具执行前检查取消标记,随后等待资源锁;如果等待期间发生取消,拿到锁之后是否还会调用工具?提案试图在获得锁后再检查一次。核验截至 2026 年 9 月 6 日,该 PR 已关闭、未合并,作者说明将重新提交并修正证据。它是研究线索,不是本文独立复现的上游事故,也不能写成已经交付的修复。

它照亮的问题很有价值:检查与动作之间如果隔着等待,先前判断是否仍然有效?

例如,工具要使用一个被其他任务占用的环境。进入队列时可以运行,不等于轮到它时仍应运行。但要判断系统是否真的有问题,必须先回答:取消入口承诺撤销什么?请求有没有成功?等待发生在哪一层?

因此,我们没有直接给自己的系统加一个“再检查一次”的功能,而是先测已有边界。

2. 第一个反例:过了期限执行,不一定是授权过期后执行

CodeFlowMu 是我们在开发的本地多 Agent 协作系统,围绕任务文件、执行会话和证据组织工作。本轮使用固定源码基线 c008d9db91a21136fc61a4f60314e22db395d5d2,在独立合成目录里调用真实审批服务;没有取消用户的在线任务,也没有修改产品代码。

先测正常路径:批准一个精确的文件操作,执行一次,再尝试重复执行。两轮都是首次成功、文件效果一次;第二次被拒绝,返回 APPROVAL_ALREADY_CONSUMED,即这份批准已经被消费。

然后测试时间。我们通过服务提供的时钟注入接口,把待审批期限设为 30 秒,不靠随机等待制造竞态。

场景实际结果,两轮一致能得出的判断
尚未批准,先取消,再尝试执行请求进入取消状态;未签发凭据的执行尝试被拒绝;文件效果 0 次验证了取消状态与未获批执行的拒绝,未验证已批准令牌的撤销
一直未批准,超过 30 秒状态变为过期;批准被拒绝;文件效果 0 次待审批期限有效
期限内批准,第 31 秒执行执行成功;文件效果 1 次需要核对期限约束的对象,不能直接报漏洞

第三行看上去最可疑:记录里的 expires_at 是第 30 秒,为什么第 31 秒还可以执行?

第一行也有一个需要保留的前提:请求还没有获批,因此执行尝试使用的不是已签发令牌。这一组验证待审批取消和未获批执行的拒绝,不能拿来证明“已经批准的令牌可以撤销”。

源码给出了明确答案:这一期限约束的是等待审批的时间。既有测试还明确要求,已经批准的令牌可以在待审批截止时间之后使用。令牌在这里是执行时出示的批准凭据,不是对所有操作都承诺相同失效规则的通行证。

于是,本次观察不能命名为“绕过授权有效期”。准确的说法是:待审批截止时间,不是批准后的通用执行截止时间。

如果产品将来需要“批准十分钟后也不得执行”,那是另一条应当明确制定并验收的规则。不能先给现有字段赋予它没有承诺的含义,再把结果当成安全缺陷。

3. 第二个反例:执行器在等,取消为什么被拒绝?

更关键的实验,是在审批服务已经进入执行器回调后,放置一个由脚本控制的等待点。回调可以理解为审批服务交给具体执行代码的入口。我们先暂停回调,发出取消请求,最后放行。

这里必须说清实验条件:等待是研究脚本注入的。受测本地文件执行器使用同步文件操作,本轮没有复现 OpenHands 式的真实资源锁队列。

场景取消回执最终文件效果解释
回调进入后等待,取消,再放行APPROVAL_NOT_PENDING,取消被拒绝1 次不是“成功取消后仍执行”
相同等待,但研究回调在放行后检查自设中止标记取消仍被拒绝0 次,回调抛错说明动作前检查可以改变结果,不是产品新增能力
文件效果已经发生,等待收尾时再取消取消被拒绝已发生的 1 次效果仍在不能把事后请求当成自动回滚

三组场景各在新建的独立实验目录中运行两轮,结果一致。

取消回执与实际文件效果的三组受控实验对照

图 1:对应 A1、A4、A5:第三行的额外中止检查仅由研究回调提供。等待不是产品真实资源队列;待审批场景没有已签发令牌。 来源:本文受控实验,AI 绘制的解释图。

查看文中图原图

为什么第一组取消失败?审批服务在校验批准状态、令牌和请求摘要后,先把记录推进为 executing,表示进入执行器阶段,再调用回调。而其取消接口只接受尚待处理的审批。

所以,等待时的 executing 不证明文件已经写入;APPROVAL_NOT_PENDING 则确实证明这次审批取消没有成功。两条事实必须分别保留。

第二组是有意设置的机制对照:研究回调在等待结束后看到中止标记,于是没有调用真实文件执行器。它说明改变检查位置可以改变结果;这里起作用的是研究脚本的中止标记,不是被拒绝的审批取消请求。

这也是本轮研究的分水岭。“发出了取消请求”不能替代“取消已经被接受”;“进入执行器”也不能替代“效果已经发生”。

4. 已有保护不能因为新选题而被忽略

审批服务不是整个执行链。CodeFlowMu 还有连接编程 Agent 的会话适配器,负责接收请求、处理审批答复以及结束会话。

我们进一步测试了自己的 Codex 会话适配器:使用真实 CodexAppServerRunHandle 类,接入内存中的伪进程通信流,不启动真实 Codex,也不执行终端工具。

观察点只有一个:最终是否发送批准答复。

场景每轮实际发送的批准答复
正常请求,答复前未取消1 次
请求已经收到,异步处理尚未返回时取消会话0 次
会话取消后,迟到请求才到达0 次

这三个场景各跑两轮。结果说明,在受测适配器边界,会话结束后存在阻止迟到答复的保护。这里不应再宣称“系统完全没有最后一道检查”。

但答复没有发出,与真实宿主内部的工具没有执行,仍然是不同命题。宿主指实际承载 Agent 和工具的执行程序。如果答复已经交给它,随后工具又等待资源,那个等待窗口里的规则还需要在具体宿主中验证。

本地适配器做对一件事,不能替另一个进程提供保证;同样,外部项目发现一个窗口,也不能抹掉本地已经验证的保护。

5. 取消应该留下怎样的证据?

如果要继续研究真正的工具队列,最有区分力的记录不是最后一个 cancelled 状态,而是一条能够对齐的时间线:

取消请求到达 → 接受或拒绝的回执 → 等待结束 → 最后一次有效性检查 → 真实动作开始 → 效果与收尾。

这是一份后续实验建议,不是本轮已经实现的统一接口。

每一步回答不同的问题:接受回执说明系统承诺了什么;动作开始点说明承诺是否来得及生效;效果记录说明是否还有需要对账或补救的事情。已经发生的外部动作,需要另行定义补偿,不能仅靠停止后续计算把它变成“没发生”。

对工程团队,最值得先检查的也不是“哪里再插一个取消标记”,而是下面三件事:

  • 界面有没有把“正在请求取消”提前显示成“已经停止”?
  • 每个取消入口到底针对待审批、整个会话,还是某个正在运行的动作?
  • 对资源等待,能否证明检查发生在等待之后、动作之前,而不只是函数入口?

本轮把已有保护与待测窗口分开了:审批取消只处理待审批请求,会话适配器能够阻止受测场景中的迟到答复,而答复已经交出后的真实工具队列仍需另测。下一步应沿这条边界找证据,再决定是否开发。

可靠的取消,不是让所有地方都写着“已取消”,而是能说明:请求在哪一层被接受,阻止了哪一步,还有什么事实没有被改变。

证据与范围

本篇使用 7 个审批场景和 3 个适配器场景,各两轮;不是生产事故统计,也不是安全准确率。研究还对由两个既有测试文件组成的基线测试集运行两轮,每轮合计 39 项通过、0 失败、0 跳过,不能把它们相加成端到端保证。

第一方原始记录为 boundaries-1788695589568.json 的 A0–A6 和 adapter-1788695732064.json;适配器正常场景最后的取消状态来自测试清理,不是正常执行失败。双语证据包提供全部脱敏观测、来源、两轮基线日志、研究探针和完整性校验。公开包去除本机路径和进程编号,保留结果与顺序;运行产品探针仍需有权访问固定源码及其依赖,不能把记录校验当成产品复跑。本文写作进行了记录复核,没有新增真实宿主测试。

真实资源队列、操作系统进程退出、外部效果撤销以及授权变更后的实际工具行为,均不在本轮已验证范围内。

Last updated: