Files
group_fqcd_jr/app/service/agent/implementations/platform_probe.py
T
lzf_0626 edc0c43245 test(base): 造只读探针端到端验证工具拒绝,并修掉它暴露的一个死分支
**为什么造探针**:ToolExecutor 的四种拒绝在真实链路上很难安全触发——要么改客服、风控的
生效配置,要么动 RBAC,两条路都会影响正在工作的 Agent。platform_probe 是个只读、无副作用
的探针:它只声明 probe 一个意图(所以意图分类只可能返回它)、没有发布工具白名单
(天然处于"未配置"状态)、工具只回显参数不碰业务数据。

**它立刻查出一个死分支**:探针报的是「工具不在当前意图白名单」,而不是我新加的
「该意图未配置工具白名单」。原因是 governance.resolve 会为每个 supported_intents
**预填条目**(governance.py:55-61),未配置时得到的是**空元组**——所以
intent not in configured_tools 在运行期**永远不成立**,那个分支是死代码。

单元测试没能发现它,因为我在测试里手工构造了 configured={},而真实链路不产生这个形状。
**这正是端到端测试的价值**:单元测试验证的是我设想的形状,端到端验证的是真实形状。

修法:改判"白名单为空"而非"缺键",文案改为"该意图的工具白名单为空",并注明经过 governance
装配后"完全没配"与"配了空列表"无法区分、也不假装能区分(两者运维动作相同)。新增一条按
**真实形状**({"faq": ()})构造的用例把它锁住。

实测:探针调用 → failed / AGENT_PERMISSION_DENIED,stderr 为
ForbiddenAgentError: 该意图未配置工具白名单(tool_executor.py:108)。

ruff / mypy(136 文件) / 611 unit+contract 全绿。
2026-09-11 13:10:04 +08:00

67 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""基座验证探针:端到端触发 `ToolExecutor` 的四种拒绝分支。
**为什么专门做一个 Agent**:`ToolExecutor` 的四种拒绝(意图未配置工具白名单 / 工具不在
白名单 / 缺权限 / 角色不符)在真实链路上很难**安全**触发 —— 要么去改客服、风控的生效配置,
要么去动 RBAC,两条路都会影响正在工作的 Agent。用一个只读、无副作用的探针把这件事隔离出来。
**它不碰任何业务数据**:唯一的工具只回显调用方给的参数,不查库、不调模型、不写状态。
角色限定为 `admin`,意图只有 `probe` 一个。
用法见 `tools/verify_tool_executor_denials.py`:它按顺序调整探针的意图配置与工具白名单,
分别触发四种拒绝,最后把配置恢复到验证前的状态。
"""
from typing import Any
from pydantic import BaseModel, ConfigDict
from app.core.contracts import (
AgentDefinition,
AgentRequest,
CoreResult,
RequestContext,
)
from app.service.agent.base import BaseAgent
AGENT_TYPE = "platform_probe"
INTENT_PROBE = "probe"
PROBE_TOOL = "probe_echo"
PROBE_PERMISSION = "probe:read"
class ProbeEchoArgs(BaseModel):
"""严格入参:探针不接收自由文本,避免被当成通用执行入口。"""
model_config = ConfigDict(extra="forbid", frozen=True)
note: str = ""
async def probe_echo_tool(arguments: BaseModel, context: RequestContext) -> dict[str, Any]:
"""回显参数。**只读且无副作用** —— 不查库、不写状态、不调外部服务。"""
note = getattr(arguments, "note", "")
return {"echo": note, "trace_id": context.trace_id}
class PlatformProbeAgent(BaseAgent):
"""基座探针:只用于验证工具链路的拒绝行为,不承载任何业务。"""
definition = AgentDefinition(
agent_type=AGENT_TYPE,
version="1.0.0",
allowed_roles=("admin",),
allowed_portals=("api",),
allowed_tools=(PROBE_TOOL,),
supported_intents=(INTENT_PROBE,),
)
async def handle(self, request: AgentRequest, context: RequestContext) -> CoreResult:
# 走公共工具链路:探针要验证的正是这条路上的白名单、权限与角色校验。
output = await self.call_tool(
PROBE_TOOL,
{"note": request.message[:50]},
intent=INTENT_PROBE,
context=context,
)
return CoreResult(text=f"probe ok: {output}", intent=self._classified_intent)