
Agent 有工具权限,为什么还要为每一次执行重新核对?从 GitHub MCP 到任务证据链
Agent 拥有什么工具回答的是原则上能做什么;一次具体执行是否应该发生,还要核对当前任务、目标、版本、批准与结果。
Agent 有工具权限,为什么还要为每一次执行重新核对?从 GitHub MCP 到任务证据链
Agent 拥有什么工具,回答的是“原则上能做什么”;一次具体执行是否应该发生,回答的是“这一次、对这个目标、依据什么,可以做什么”。两者之间,需要一条可以重新核对的证据链。
先说结论
我们最近在 CodeFlowMu 里修复了一个看起来很小的问题:一张原本可以继续执行的任务,被安全门禁错误挡住了。
CodeFlowMu 是一个本地运行的多 Agent 协作系统,用任务、角色、门禁、报告与审批,把多个 Agent 的工作组织成可追踪、可恢复、可验证的执行链。
这次问题并不是 Agent 没有工具权限,也不是任务真的越界。真正发生的是:一条本来只应该提醒负责人的信息,被运行时错误地升级成了“禁止执行”。
这次事故让我们重新检查一个更基础的问题:
如果 Agent 已经拥有 Git、Shell 或其他工具能力,为什么系统还需要在每一次具体执行发生时重新核对?
与此同时,GitHub 官方 MCP Server 也在另一个授权层上处理非常相似的问题。GitHub MCP 面对的是 OAuth 权限,CodeFlowMu 面对的是任务执行资格。两者不是同一个系统,也不是同一种授权模型,但它们正在从不同方向碰到同一个边界:
静态能力 → 当前调用 → 当前上下文 → 本次授权判断
也就是说:“有能力使用某个工具”,不能自动推出“这一次调用就有权执行”。
本文不是要宣布 CodeFlowMu 已经建立完整的 Agent 授权体系。我们更关心的是一种正在真实工程中出现的收敛:两个独立系统,都开始把授权判断从“主体拥有什么能力”,推进到“当前这一次执行是否仍然有资格发生”。 CodeFlowMu 这一边,不只有观点,还有真实失败、修复过程、回归数据和现场检查留下来的证据。
1. 一道安全检查,为什么反而挡住了合法任务?
CodeFlowMu 的一条典型工作链可以简化为:
需求 → PM → 任务 → DEV / OPS / QA → 报告 → 审核 / 批准 → 完成
这样的系统当然需要门禁。根任务已经关闭、目标超出任务范围、明确要求的正式批准还没有发生、必须满足的依赖仍然缺失,这些都可能构成合理的拒绝理由。
问题出在另一类信息。例如“最好补一份验收说明”或者“建议先把计划写得更完整”。这些信息同样有价值,但它们表达的是建议,不是禁令。
这次 CodeFlowMu 的问题恰好发生在这里:一条本来只有提醒意义的信息,被运行时解释成了硬门禁。本来应该是“发现信息不完整 → 提醒负责人 → 继续由有权的人判断”,最终却变成了“发现信息不完整 → 判定条件不满足 → 禁止继续执行”。
Agent 没有越权,任务也没有越界。真正越过边界的,是安全逻辑自己。
这也是为什么我们后来发现,真正需要区分的并不是简单的“通过 / 不通过”,而是什么事实有资格直接产生拒绝。
| 条件 | 性质 | 运行时行为 |
|---|---|---|
| 根任务已经关闭 | 硬约束 | 拒绝 |
| 目标超出任务范围 | 硬约束 | 拒绝 |
| 明确要求的正式批准缺失 | 硬约束 | 拒绝 |
| 必需依赖未满足 | 硬约束 | 拒绝 |
| 计划说明不完整 | 建议性条件 | 提醒 |
| 建议补充验收说明 | 建议性条件 | 提醒 |
| 建议负责人再次确认 | 复核信号 | 转交判断 |
一个系统拥有拦截能力,并不意味着它拥有对所有“不理想状态”作出拒绝的权力。所以这次事故最终留下的原则很简单:
建议不等于禁令。只有真正拥有拒绝权的事实,才应该足以产生拒绝。
真正可靠的安全系统,不只要知道什么时候应该关门,还必须知道什么时候自己没有资格关门。
2. 为什么 87 / 87 通过以后,我们仍然没有宣布修好了?
第一轮定向检查很快通过:87 / 87。如果目标只是拿到一个漂亮数字,到这里似乎已经可以结束。但一个门禁不是孤立函数,它会沿着完整链路传播:
规则 → Runtime → 接口 / Shell → 真实进程 → 页面
任何一层仍然保留旧语义,真实系统就可能继续给出错误结果。这次修复过程恰好证明了这一点:
| 阶段 | 结果 | 暴露的问题 |
|---|---|---|
| 定向派工检查 | 87 / 87 | 已覆盖的目标路径通过 |
| Runtime 首轮全量 | FAIL | 旧任务仍被新门禁机械阻断 |
| Runtime 修正后 | 1702 / 1702 | 指定 Runtime 回归集通过 |
| Shell 首轮全量 | FAIL | 页面相关测试仍保留旧状态语义 |
| Shell 修正后 | 936 / 936 | 指定 Shell 回归集通过 |
| 第一次现场检查 | FAIL | API 缺少页面仍依赖的兼容字段 |
| 修正并再次受控重启 | PASS | 运行时、接口和页面重新对齐 |
这里最值得注意的不是 1702,而是:1702 / 1702 通过以后,真实系统仍然存在问题。
运行时已经给出了正确状态,但 API 没有继续输出页面仍然需要的兼容字段,结果页面把“缺少字段”显示成了“冲突”。所以:测试通过,不等于系统正确。
这也是为什么我们保留第一次失败、第二次失败以及现场检查记录。如果只留下最终 PASS,很多真正有价值的工程信息反而会消失。
3. 这不是一个孤立 Bug,而是一串持续出现的边界问题
如果只有这一次误拦,很容易把它归结成“某个判断条件写错了”。但 CodeFlowMu 最近一段时间的工程记录并不是这样。
围绕任务治理,我们连续处理过:建议性计划不能自动成为否决条件;新治理规则不能机械阻断历史任务重放;批准历史怎样进入治理视图;批准记录怎样保持身份和内容一致;Session 恢复以后哪些旧事实仍然有效;Runtime 和页面怎样保持同一个任务状态解释;Retry / Recovery 以后旧状态能不能直接继续使用。
这些问题表面不同,却不断指向同一个更深的结构:一次动作能不能发生,不能只由 Agent 的角色和工具列表决定。 它还取决于当前任务、当前目标、当前版本、之前的批准是否仍然有效、当前条件究竟是建议还是硬拒绝,以及发生重试时是否可能再次产生副作用。
所以 CodeFlowMu 的研究过程不是“先提出完整理论,再做一个 Demo”,而更接近:
真实问题 → 定位原因 → 修改规则 → 实现修复 → 回归测试 → 现场验证 → 暴露新边界 → 继续研究
这也是我们把它称为工程研究的原因。不是先有结论,再找例子;而是系统真的撞到了边界,我们修复它、留下数据,再从重复出现的问题中逐步抽象模型。
4. GitHub MCP 给了一个很有价值的外部参照
GitHub 官方 MCP Server 面对的不是 CodeFlowMu 的任务治理,它主要处理 GitHub Tool 与当前凭据之间的权限关系,但它没有把所有授权问题处理成一种模式。
例如,GitHub MCP Server 的工具会声明自己可能需要的 OAuth 权限范围。不同工具需要的权限并不相同:有的需要 repo,有的需要 read:org,安全相关工具还可能需要 security_events。这意味着 Tool 本身可以声明一个基础授权边界。
更有意思的是,GitHub 对不同认证方式采用了不同策略。对于 classic PAT,可以在启动时读取 token 已有的 OAuth scope,并据此过滤 Tool:当前凭据明显没有相应权限时,对应工具可以不展示。
但 remote OAuth 并不只做这一层检查。真正调用 Tool 时,如果当前 OAuth token 缺少这一次调用所需要的权限,Server 可以返回权限范围挑战(scope challenge):
Tool 已经可用 → 发起当前调用 → 检查当前 OAuth 权限 → 足够则执行,不足则请求追加授权
GitHub MCP Server 的官方文档明确区分了这两种机制:classic PAT 是启动时的 Tool filtering;remote OAuth 则可以在具体 Tool Call 发生时重新检查 scope,当前授权不足时再发起 challenge。
GitHub 的 HTTP Server 还提供显式的 --scope-challenge 模式。在权限不足时,可以返回 403 Forbidden 和 WWW-Authenticate,指出当前请求需要的权限范围。
这里最重要的并不是 OAuth 本身,而是授权判断发生的位置:
当前这一次 Tool Call,本身可以成为授权检查点。
授权没有在“这个客户端已经连接了 GitHub MCP”或者“这个 Tool 已经对 Agent 可见”的时候永久结束。真正调用发生时,系统仍然可以重新问:当前已有授权,对这一次调用够不够?
这和 CodeFlowMu 在任务治理中遇到的问题非常接近。
5. 每一次重新核对,不等于每一次重新让用户批准
这里很容易产生误解。如果每执行一条命令,都弹一次“是否批准”,那不是一个成熟的授权系统,只是把所有判断重新推给用户。
本文说的“每一次重新核对”,真正指的是:每一次准备产生副作用时,系统都重新检查当前已经存在的授权事实是否仍然足够。
例如,任务仍然有效、目标仍在范围内、版本没有变化、批准仍然有效、当前动作仍被允许,那么系统完全可以自动继续执行,不需要用户重新点击一次批准。只有当某些事实发生变化,或者当前所需授权不足时,才进入追加授权、复核或拒绝。
所以:重新核对 ≠ 重新批准。
GitHub MCP 的 scope challenge 本身就是这个思路:每次调用都可以重新检查,但只有当前 scope 不足时,才请求追加授权。
6. GitHub MCP 和 CodeFlowMu,到底哪里相同?
两者解决的不是同一层问题。
| GitHub MCP | CodeFlowMu |
|---|---|
| 当前 Tool | 当前 Agent |
| 当前 Tool Call | 当前具体动作 |
| 当前调用参数 | 当前任务与目标 |
| 当前 credential | 当前任务版本与批准 |
| 检查 OAuth scope 是否足够 | 检查任务证据是否足够 |
| Challenge / Execute | 拒绝 / 复核 / 执行 |
GitHub MCP 主要回答:“当前 credential 是否拥有本次 Tool Call 所需要的权限?”CodeFlowMu 需要回答:“当前 Agent 在当前任务上下文里,有没有资格让这次动作产生副作用?”
两者不能画等号,但它们共享一个结构性问题:静态能力不够。
GitHub MCP:已有 Tool → 当前 Tool Call → 核对当前 scope → 追加授权 / 执行
CodeFlowMu:Agent 已有 Tool → 当前具体动作 → 核对任务证据 → 拒绝 / 复核 / 执行
真正相同的是:当前上下文必须重新进入判断,静态能力不能单独决定执行资格。 这不是谁借鉴谁,也不是 GitHub MCP 已经实现了 CodeFlowMu 的任务授权,而是两个独立工程系统从不同授权层面碰到了同一种边界。
PR #3128 的位置
GitHub MCP Server 后续合并的 PR #3128 又向前走了一步:同一个 Tool,因为本次调用参数不同,所需要的 OAuth scope 也可能不同。
例如,写普通仓库文件和修改 GitHub Actions workflow 文件,即使经过同一个文件写入 Tool,后者仍可能需要额外的 workflow scope。这进一步说明:真正影响授权判断的,不只是 Tool 名字,还包括这一次调用到底在做什么。
但 CodeFlowMu 的工程问题不是从 #3128 才开始。我们的任务门禁、批准和状态问题此前已经在真实运行里持续出现,并留下了独立记录。因此 #3128 在本文中的位置是进一步的工程参照,而不是 CodeFlowMu 的实现来源,也不是本文提出任务级执行授权问题的前提。
7. CodeFlowMu 真正需要重新核对的,是任务事实
GitHub MCP 主要核对 OAuth scope。CodeFlowMu 需要重新核对的,则是任务上下文,至少有五类事实不能省略。
第一,任务绑定。 当前动作属于哪张任务?TASK-102 允许修改 repo-A,不能自动推出 DEV 可以顺手修改 repo-B。Agent 的工具能力属于角色,行动资格属于当前任务。
第二,目标绑定。 即使都在同一个仓库,repo-A / feature-x 和 repo-A / main 也可能对应完全不同的风险和批准要求。所以“DEV 有 Git”远远不够回答“这一次 push 是否允许”。
第三,当前有效性。 假设 10:00 时 Task=T1、Version=V1、Approval=valid;到了 10:15,Task 仍然是 T1,但 Version 已经变成 V2,那么之前的批准是否仍然有效就需要重新判断。重新核对不是不信任 Agent,而是不把针对旧事实作出的授权自动延伸成新权限。
第四,决策性质。 运行时至少需要分清“允许、建议、需要复核、必须批准、拒绝”。一句“最好补一份验收说明”,不能仅凭自己就推出“禁止执行”。
第五,重试与副作用。 即使授权完全正确,也不能推出外部副作用恰好发生一次。一次 push 超时以后,第一次到底根本没有执行,还是已经执行只是响应丢失,是两个完全不同的结果。因此调用级授权最终还需要和本次动作、本次尝试、幂等性以及外部执行结果建立联系。
这五类问题共同说明:授权正确 ≠ 外部副作用已经正确发生。
8. 从“工具权限表”,走向“任务证据链”
静态权限表通常可以写成:DEV → Git | OPS → Shell | QA → Test。它回答的是“谁原则上拥有什么能力”,但无法回答“为什么 DEV 现在可以对 repo-A/main 执行这一条 push”。
真正需要的,是更具体的任务证据链:
Agent 能力 → 当前任务 → 当前动作 → 当前目标 → 当前版本 → 当前批准 → 授权判断 → 实际执行 → 外部结果
这就是标题里“从 GitHub MCP 到任务证据链”真正想表达的内容。GitHub MCP 在 Tool Call 时重新核对当前 credential 的 OAuth scope;CodeFlowMu 则需要继续追问:这一次动作在当前任务上下文里的执行资格,到底来自哪里?
这也是为什么我们开始研究一种更窄的对象:执行授权回执(Execution Authorization Receipt)。概念上,它可能包含:
{
"task_id": "TASK-...",
"occurrence_id": "CALL-...",
"actor": "DEV",
"tool": "git",
"action": "push",
"target": {
"repository": "...",
"branch": "..."
},
"task_version": "...",
"decision": "ALLOW",
"evidence": ["..."],
"expires_at": "...",
"idempotency_key": "...",
"effect_ref": "..."
}字段名称还不是重点,真正重要的是:不同事实不能互相冒充。 Agent 有 Git,不等于这一次 push 已获授权;Agent 说“用户已经批准”,不等于批准证据真实有效;授权结果是 ALLOW,不等于外部动作已经发生;Tool 返回成功,也不等于业务结果已经验收;存在幂等键,同样不等于外部系统已经保证恰好执行一次。
这就是任务证据链比静态权限表多出来的东西。
9. 现在已经证明了什么,又还没有证明什么?
V1.9.6 已有工程证据支持:历史任务被新门禁机械阻断的问题确实出现过;建议性条件与 Runtime 硬门禁的边界被重新处理;调度相关定向检查达到 87 / 87;Runtime 指定全量回归达到 1702 / 1702;Shell 首轮仍存在旧语义断言;修正后 Shell 指定全量达到 936 / 936;第一次现场检查又发现 API → UI 状态兼容问题;修正以后再次完成全量回归和受控重启。
但这些证据不能推出:所有 Tool Call 都已经拥有执行授权回执;所有高风险动作都已经完成调用级授权;所有批准都具备完整主体身份验证;所有授权都有密码学来源证明;Retry / Resume / Delegation 都已经完整重新核对;所有外部副作用都已经获得“恰好一次”保证;CodeFlowMu 已经拥有完整通用的授权账本。
这些仍然属于下一阶段工程工作。
为什么要把这个边界写得这么清楚?因为真实工程过程已经告诉我们:87 / 87 之后仍然可能有 Runtime 全量失败;1702 / 1702 之后仍然可能有 Shell 问题;936 / 936 之后第一次现场检查仍然可能发现 API / UI 不一致。
所以真正有价值的不是某个最终数字,而是:失败 → 定位 → 修复 → 回归 → 再次暴露问题 → 再次修复 → 现场验证。
我们也不会把 87、1702 和 936 相加,制造一个所谓“整个系统成功率”。它们属于不同测试集合,每一个 PASS 只能证明自己覆盖的那部分问题。这与本文讨论的授权问题遵循的是同一个原则:
一个事实能证明什么,就只让它证明什么。
最后:为什么有了工具权限,还要重新核对?
现在可以回到标题。Agent 已经有工具权限,为什么还要为每一次执行重新核对?
因为“有工具权限”只回答:“它原则上能不能使用这种能力?”而真正执行时还存在另一个问题:为什么这一次、对这个目标、在当前任务状态下,可以让这种能力产生副作用?
GitHub MCP 在 OAuth 层给出的答案之一是:当前 Tool Call 再检查当前 token scopes,不够就 challenge。CodeFlowMu 在任务治理层正在形成的答案则是:当前动作重新核对当前任务证据,不能因为 Agent 已经拥有 Tool,就自动认为本次执行具有资格。
两个系统处理的授权层不同,但它们都指向同一个边界:静态能力 ≠ 当前执行授权。
这次 CodeFlowMu 的误拦没有证明我们已经解决完整的 Agent Authorization 问题。它让我们更清楚地看见了另一件事:
真正可靠的 Runtime,不仅要防止 Agent 做它无权做的事,也必须防止安全逻辑替真正的决策者拒绝一件本来有权发生的事。
而持续的失败记录、回归数据和现场验证,正在把这个问题从一个 Bug,逐渐变成一个可以研究、可以测试、也可以继续实现的执行边界模型。
最终,一个成熟的 Agent Runtime 应该能够回答:谁,在什么任务里,准备执行什么动作,针对什么目标,依据什么仍然有效的授权事实,为什么现在可以执行,以及最终实际发生了什么。
所以:
能力属于 Agent;授权属于具体执行;证据必须跟着执行走。
公开证据
来源与证据边界
GitHub MCP Server
GitHub MCP Server:PAT Scope Filtering / OAuth Scope Challenges
本文引用其对启动时 Tool filtering 与调用时 scope challenge 的区分。remote OAuth 场景中,具体 Tool Call 可以重新检查当前 scope;当前授权不足时,再请求追加授权。GitHub MCP Server:Streamable HTTP Server
HTTP 模式支持--scope-challenge;权限不足时可以返回403 Forbidden和WWW-Authenticate,指出所需权限范围。Sam Morrow 的 GitHub MCP Server PR #3128
2026-08-26 复核。作为进一步工程参照,该 PR 将部分 scope 判断推进为按本次调用参数计算:同一个 Tool 可以因为调用目标不同而需要不同 scope。本文不把它视为 CodeFlowMu 的实现来源,也不以它单独证明任务级执行授权模型。
CodeFlowMu
本文关于 CodeFlowMu V1.9.6 的工程结论,仅适用于本文所述版本、测试集合和受控现场检查。
公开 Execution Boundary 数据包保留了测试集合、首次失败、修复、再次验证、汇总口径、脱敏现场观察以及明确的“不支持什么”边界。更完整的内部研发过程还包括设计任务、Runtime / Shell 回归、治理修复和运行记录;公开材料只使用能够脱敏并独立说明问题的部分。
原始日志、任务正文和本机路径不公开。
本文不是独立安全审计,也不是通用 Agent 授权证明;它不证明所有 Tool Call 已经完成调用级授权,不证明完整主体身份验证或密码学授权来源,也不证明外部副作用的“恰好一次”执行,更不构成对未来版本行为的保证。
研究结论应始终与对应版本、测试集合和工程证据一起阅读。