Files
group_fqcd_jr/app/service/agent/implementations/platform_probe.py
T
lzf_0626 4e2e42c896 test(base): 验证最后一种工具拒绝(角色不符),四种分支全部实测通过
分支 4 是最难构造的一种,两个前提缺一不可:

1. **工具的角色集合必须比 Agent 的更窄**。Agent 层的 validate_access(base.py:101)会先按
   AgentDefinition.allowed_roles 拦截,两者一致时永远进不到工具层的角色校验。所以把
   probe_alt 收窄为 ("risk_operator",),而 Agent 仍允许 admin。
2. **调用者必须有工具要求的权限**,否则会先命中权限分支。所以脚本临时给 admin 授
   probe:read,验证后撤销。

过程中又修掉一处自己写错的地方:探针的 handle 原先硬编码调用 PROBE_TOOL,导致分支 4
(需要调 probe_alt)与分支 2(需要调白名单之外的那一个)互相干扰——第一次跑出来的结果
是"工具不在当前意图白名单"。改为按消息里的 "alt" 选择要调的工具。

四种分支的实测结果,message 各自独立、指向不同处置动作:
- 白名单为空        → 该意图未配置工具白名单
- 工具不在白名单    → 工具不在当前意图白名单
- 缺少工具权限      → 缺少工具权限
- 角色不符          → 角色不能使用工具

目标的另一半也验证了:审计里是完整细节(reason = "角色 ['admin'] 与工具允许的角色
['risk_operator'] 无交集",并带 tool_name / intent / trace_id),而异常 message 只有
"角色不能使用工具"、不含角色集合。**内部配置只进审计,不进客户可见响应。**

环境复原:生效配置 9 条(与起点一致);sys_permission / sys_role_permission 中
probe:read 的行数为 0。
2026-09-11 13:12:56 +08:00

80 lines
3.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"
# 第二个探针工具。**存在的唯一理由**:让"工具不在白名单"这一分支可被触发。
# `governance.resolve` 取的是「发布白名单 ∩ 代码声明的 allowed_tools」,配置**只能缩小
# 不能放大**;所以 Agent 只有一个工具时,白名单要么为空、要么正好包含它,
# 永远构造不出"有白名单、但不含被调用的那个工具"的场景——这是我做端到端验证时撞到的。
PROBE_ALT_TOOL = "probe_alt"
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}
async def probe_alt_tool(arguments: BaseModel, context: RequestContext) -> dict[str, Any]:
"""备用探针工具:只为让"工具不在白名单"的分支可被构造,与 probe_echo 同样只读。"""
return {"alt": True, "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, PROBE_ALT_TOOL),
supported_intents=(INTENT_PROBE,),
)
async def handle(self, request: AgentRequest, context: RequestContext) -> CoreResult:
# 按消息里的 "alt" 决定调哪个探针工具。两个工具都必须可被指定调用:
# 分支 2(工具不在白名单)要调白名单之外的那个,分支 4(角色不符)要调角色更窄的
# 那个——硬编码其中一个的话,这两个分支会互相干扰(实测踩到过)。
tool = PROBE_ALT_TOOL if "alt" in request.message.lower() else PROBE_TOOL
output = await self.call_tool(
tool,
{"note": request.message[:50]},
intent=INTENT_PROBE,
context=context,
)
return CoreResult(text=f"probe ok: {output}", intent=self._classified_intent)