摘要
channel chat(Lark DM)路径上,任何 RequiresApproval=true 的工具(当前主要是 code_execute)触发审批后是永久死信 :没有审批卡、没有任何批准入口、没有 pending 持久化、没有超时,turn 照常 Completed。同时 ToolApprovalMiddleware 喂给 LLM 的工具结果写死 "An approval request has been sent."——系统先对 LLM 撒谎,LLM 再向用户编造"你批准后会继续执行"。用户视角 = 审批幻觉。
生产实证(2026-06-11 17:53 CST,Lark DM)
用户:"三分钟后给我发一下我今天的日报"
bot 回复(verbatim):
已发起 3 分钟后发送日报的执行请求,但当前环境对延时执行触发了人工审批:
code_execute → approval_required
你批准后,它会继续等待 3 分钟再执行发送;如果审批没有通过,这次延时发送不会真正执行。
[tool receipt] Approval pending: code_execute (local_request=85ad2b7dbee64220834a32308b0495f4)
console-backend 同窗口日志(15 分钟 15062 行):
85ad2b7dbee64220834a32308b0495f4 零命中;approval / code_execute 关键词零命中——"发起审批请求"动作根本不存在
该 turn 出站仅 im/v1/messages/.../reactions + channel-relay/reply,未发任何审批卡
ConversationTurnCompletedEvent(v2742)正常落盘——服务端认为本轮圆满结束,无任何挂起态
半真半假的结构(为什么极难分辨)
层
内容
真伪
工具调用
LLM 真调了 code_execute(想跑代码延时 3 分钟)
真
审批判定
NyxIdCodeExecuteTool.RequiresApproval => true 恒审批,middleware 真实挂起
真
receipt 行
[tool receipt] Approval pending: ... 由服务端 AgentRunGAgent.cs:1115 渲染
真
工具结果文案
"An approval request has been sent."
假 (此路径从未发送审批请求)
LLM 转述
"你批准后,它会继续等待 3 分钟再执行发送"
假 (无批准入口,无续传消费者)
根因(两层)
1. Yield 协议的隐含契约无人履行
ToolApprovalMiddleware.cs:155-176:ToolApprovalDecision.Yield → 产出 TerminationKind=ApprovalPending + context.PendingApproval,注释明言"Actor 层检测此 result 后持久化 pending state 并走事件化续传"。
实际消费者盘点(grep PendingApproval,排除 obj/Test):
src/Aevatar.AI.Core/RoleGAgent.cs — 唯一实现 :PendingToolApprovalPersistedEvent 持久化 + RemoteApprovalId + 状态检查 + 超时对账(workflow llm_call 路径)
agents/Aevatar.GAgents.NyxidChat/AgentRunGAgent.cs — 零命中 。只有 PendingToolCalls/PendingDropNotification;Yield 的 ToolApprovalPendingContext 直接丢弃
2. 装配层让谎言成为默认
src/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs:247(另 Bootstrap.Extensions.AI:107):
builder . Services . TryAddSingleton < IToolApprovalHandler , YieldApprovalHandler > ( ) ;
YieldApprovalHandler 全局单例注册——所有 路径都拿到"我会续传"的承诺,但只有 RoleGAgent 真的履行。channel chat 拿到 Yield 却无续传实现 → 死信。
ToolApprovalMiddleware.cs:200-209 BuildApprovalPendingResult 进一步写死:
message = "This tool requires user approval before execution. An approval request has been sent."
违反 CLAUDE.md「ACK 诚实:同步返回只承诺已达到阶段」与「抽象一旦能被滥用即设计未完成」。
workflow tool_call 链路对 ApprovalPending fail-closed(throw → step 失败),测试 WorkflowTool_WhenApprovalPending_ShouldFailClosedWithoutReturningPendingPayload 明示设计意图。channel chat 是唯一"假装等审批"的路径,比 fail-closed 更糟。
修复方案(分层)
止血(文案诚实) :BuildApprovalPendingResult 不再无条件声称 "approval request has been sent";改为"执行已暂停等待审批;若当前通道无审批入口,此工具视为不可用,请如实告知用户并建议替代路径"。
路径级 fail-closed(建议立即) :channel chat 装配 fail-closed handler(对齐 [hardening] workflow tool_call 绕过 ToolApprovalMiddleware:审批按路径而非按工具强制 #1904 哲学):审批类工具直接 Denied("当前频道无审批入口")。LLM 拿到诚实失败,receipt 渲染为 Denied 而非悬空 pending。
架构级治理(彻底杜绝) :YieldApprovalHandler 不得全局 TryAddSingleton。Yield 是带契约的协议("actor 必须实现续传"),handler 跟随 actor 能力装配:只有实现了 pending 持久化+事件化续传的 actor(当前仅 RoleGAgent)注入 Yield;其余默认 MissingApprovalHandler(Denied)。补 CI guard:使用 ToolCallMiddlewareChainFactory.ForAgentRuntime 且传入 Yield 型 handler 的装配点,必须能指认 PendingApproval 消费者。
功能级(按需后做) :若产品上需要 chat 内审批,AgentRunGAgent 实现真续传:持久化 pending state → 发 Lark 审批卡(FeishuCard 链路现成)→ 卡片回调事件化续传 → durable timeout,对齐 RoleGAgent 模式。在此之前 fail-closed 是诚实的。
验收建议
单测:channel chat 链路中 RequiresApproval=true 工具调用 → 工具结果为明确 Denied/不可用语义,不含 "approval request has been sent" 类承诺;receipt 状态非 ApprovalRequired。
回归:Lark DM 实测同句式请求,bot 回复不再出现"等你批准"叙事。
guard:扫描禁止 Yield handler 注入到无 PendingApproval 消费者的执行环境。
关联
摘要
channel chat(Lark DM)路径上,任何
RequiresApproval=true的工具(当前主要是code_execute)触发审批后是永久死信:没有审批卡、没有任何批准入口、没有 pending 持久化、没有超时,turn 照常 Completed。同时ToolApprovalMiddleware喂给 LLM 的工具结果写死"An approval request has been sent."——系统先对 LLM 撒谎,LLM 再向用户编造"你批准后会继续执行"。用户视角 = 审批幻觉。生产实证(2026-06-11 17:53 CST,Lark DM)
用户:"三分钟后给我发一下我今天的日报"
bot 回复(verbatim):
console-backend 同窗口日志(15 分钟 15062 行):
85ad2b7dbee64220834a32308b0495f4零命中;approval/code_execute关键词零命中——"发起审批请求"动作根本不存在im/v1/messages/.../reactions+channel-relay/reply,未发任何审批卡ConversationTurnCompletedEvent(v2742)正常落盘——服务端认为本轮圆满结束,无任何挂起态半真半假的结构(为什么极难分辨)
code_execute(想跑代码延时 3 分钟)NyxIdCodeExecuteTool.RequiresApproval => true恒审批,middleware 真实挂起[tool receipt] Approval pending: ...由服务端AgentRunGAgent.cs:1115渲染"An approval request has been sent."根因(两层)
1. Yield 协议的隐含契约无人履行
ToolApprovalMiddleware.cs:155-176:ToolApprovalDecision.Yield→ 产出TerminationKind=ApprovalPending+context.PendingApproval,注释明言"Actor 层检测此 result 后持久化 pending state 并走事件化续传"。实际消费者盘点(grep
PendingApproval,排除 obj/Test):src/Aevatar.AI.Core/RoleGAgent.cs— 唯一实现:PendingToolApprovalPersistedEvent持久化 +RemoteApprovalId+ 状态检查 + 超时对账(workflow llm_call 路径)agents/Aevatar.GAgents.NyxidChat/AgentRunGAgent.cs— 零命中。只有PendingToolCalls/PendingDropNotification;Yield 的ToolApprovalPendingContext直接丢弃2. 装配层让谎言成为默认
src/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs:247(另Bootstrap.Extensions.AI:107):YieldApprovalHandler全局单例注册——所有路径都拿到"我会续传"的承诺,但只有 RoleGAgent 真的履行。channel chat 拿到 Yield 却无续传实现 → 死信。ToolApprovalMiddleware.cs:200-209BuildApprovalPendingResult进一步写死:违反 CLAUDE.md「ACK 诚实:同步返回只承诺已达到阶段」与「抽象一旦能被滥用即设计未完成」。
对比:workflow 路径的正确姿势(#1904 / d5ef0c3)
workflow tool_call 链路对
ApprovalPendingfail-closed(throw → step 失败),测试WorkflowTool_WhenApprovalPending_ShouldFailClosedWithoutReturningPendingPayload明示设计意图。channel chat 是唯一"假装等审批"的路径,比 fail-closed 更糟。修复方案(分层)
BuildApprovalPendingResult不再无条件声称 "approval request has been sent";改为"执行已暂停等待审批;若当前通道无审批入口,此工具视为不可用,请如实告知用户并建议替代路径"。Denied("当前频道无审批入口")。LLM 拿到诚实失败,receipt 渲染为 Denied 而非悬空 pending。YieldApprovalHandler不得全局TryAddSingleton。Yield 是带契约的协议("actor 必须实现续传"),handler 跟随 actor 能力装配:只有实现了 pending 持久化+事件化续传的 actor(当前仅 RoleGAgent)注入 Yield;其余默认MissingApprovalHandler(Denied)。补 CI guard:使用ToolCallMiddlewareChainFactory.ForAgentRuntime且传入 Yield 型 handler 的装配点,必须能指认PendingApproval消费者。AgentRunGAgent实现真续传:持久化 pending state → 发 Lark 审批卡(FeishuCard 链路现成)→ 卡片回调事件化续传 → durable timeout,对齐 RoleGAgent 模式。在此之前 fail-closed 是诚实的。验收建议
RequiresApproval=true工具调用 → 工具结果为明确 Denied/不可用语义,不含 "approval request has been sent" 类承诺;receipt 状态非 ApprovalRequired。PendingApproval消费者的执行环境。关联