模块 3 · 归属铁闸

G-01 归属校验 +
run_tool 留痕

理财师能查谁的数据,由 G-01 归属校验 决定。API 层越权抛 403;对话 Tool 层(对话 Tool 编排(tool_service.py))拒绝则落 blocked 行留痕,不抛异常挡整条链路。

两层校验,别混口径

层级位置拒绝时
API(四 Agent 对话 HTTP 入口(chat.py))assert_customer_accessHTTP 403 + 鉴权审计
Tool(对话 Tool 编排(tool_service.py))run_tool 内归属检查status='blocked' + agent_tool_call 留痕
advisor 规则: 角色含 advisor 时,customer_advisor_rel 里该客户与当前 actor_id 须为 active。查非名下客户 → API 403 或 Tool blocked(AUTH_403_NOT_ASSIGNED)。
CODE · 对话 Tool 编排(tool_service.py)run_tool(节选)
def run_tool(
    tool_name, agent_type, actor, customer_id,
    tool_input, session_id, trace_id=None
) -> dict:
    # 1. 归属校验(对齐 模块鉴权工厂(deps.py)assert_customer_access)
    denial = _check_access(agent_type, actor, customer_id)
    if denial:
        return _blocked_record(denial, ...)
    # 2. 执行 Core 只读 Tool 定义(core_tools.py)func
    data = spec["func"](customer_id, ...)
    # 3. agent_tool_call 落库(失败降级 warning)
    _persist_tool_call(status="success", ...)
白话

run_tool 是顾问线查数据的唯一编排入口:先验你有没有资格看这个 customer_id。

blocked 不是 403——对话里会呈现「查询被拒绝」,同时 MySQL 留一行 tool 调用记录。

customer_id 参数由 四 Agent 对话 HTTP 入口(chat.py)会话注入;Tool 不信 LLM 或用户消息里随便写的客户号。

相关文件

app/api/
模块鉴权工厂(deps.py)模块鉴权工厂 — app/api/deps.py · assert_customer_access(G-01 权威口径)
四 Agent 对话 HTTP 入口(chat.py)四 Agent 对话 HTTP 入口 — app/api/chat.py · 会话绑定 customer_id + 入口归属
app/service/
对话 Tool 编排(tool_service.py)Tool 编排 — match_intent + run_tool
顾问通用 tool→llm→guard 编排(agent_service.py)顾问通用 tool→llm→guard 编排 — tool_node 调用 run_tool
app/tool/
Core 只读 Tool 定义(core_tools.py)Core 只读 Tool 定义 — query_holdings 等

理财师在对话里查非名下客户,对话 Tool 编排(tool_service.py)会怎么做?