feat: T-03 输入防护接入 chat(T3-2)——注入/超长命中即拒 400 + input_guard_log 留痕(fail-fast 不建会话, 留痕降级不改拒绝语义), Pydantic 硬顶 20000, 集成测试 7 例

This commit is contained in:
2026-09-07 11:02:36 +08:00
parent 497554da43
commit 65b45aa917
2 changed files with 209 additions and 10 deletions
+47 -10
View File
@@ -1,18 +1,21 @@
"""对话接口(T-06 最小闭环 · FLOW §2):四 Agent 统一 chat 入口。
链路:X-Agent-Type 分流 + Agent 准入(deps.assert_agent_access,手册 §5.4)
→ customer 归属固定本人 / 代理人等指定客户走 G-01 归属校验(A-01 语义)
→ SessionGuard(会话存在、actor/agent_type 一致 AUTH_403_SESSION_AGENT、
active 状态,手册 §9)→ memory_service 窗口 → agent_service(T-07 图 +
T-04 Tool 节点:意图→归属校验→Core RO 只读查询)→ user/assistant 双消息
落 MySQL + Redis 窗口 → 响应 {session_id, reply, has_disclaimer, trace_id}。
→ T-03 输入防护(input_guard:注入短语 / 超长,命中即拒 + input_guard_log
留痕,fail-fast 在会话解析前)→ customer 归属固定本人 / 代理人等指定客户走
G-01 归属校验(A-01 语义)→ SessionGuard(会话存在、actor/agent_type 一致
AUTH_403_SESSION_AGENT、active 状态,手册 §9)→ memory_service 窗口 →
agent_service(T-07 图 + T-04 Tool 节点:意图→归属校验→Core RO 只读查询)
→ user/assistant 双消息落 MySQL + Redis 窗口 → 响应 {session_id, reply,
has_disclaimer, trace_id}。
落库:agent_session/agent_message(同 trace_id);agent_tool_call 由 Tool
节点落(T-04,success/blocked/error 全留痕)。审计:鉴权失败/越权经
deps.deny 双写留痕。
deps.deny 双写留痕;输入防护拒绝经 T-03 落 input_guard_log。
"""
from __future__ import annotations
import logging
from uuid import uuid4
from fastapi import APIRouter, Depends, Request
@@ -29,13 +32,18 @@ from app.api.deps import (
from app.repository.core_ro import CoreReadOnlyRepository
from app.repository.risk_repository import RiskRepository
from app.repository.session_repository import SessionRepository
from app.service import agent_service, memory_service
from app.service import agent_service, input_guard, memory_service
from app.utils.exceptions import ApiError, StateConflict
from app.utils.trace import current_trace
from app.utils.trace import current_trace, new_trace
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/chat", tags=["chat"])
MESSAGE_MAX_LENGTH = 4000
# Pydantic 硬顶(仅防 DoS 的超宽上限):业务上限 4000 由 input_guard
# (T-03)判定——在 guard 层拦截才能落 input_guard_log 留痕;
# Pydantic 层直接 422 会绕过留痕(F-03「拦截记录」要求)。
MESSAGE_HARD_CEILING = 20000
def _repo() -> RiskRepository:
@@ -55,7 +63,7 @@ def _core_ro() -> CoreReadOnlyRepository:
class ChatRequest(BaseModel):
session_id: str | None = Field(None, description="缺省新建会话;传入则续聊")
message: str = Field(..., min_length=1, max_length=MESSAGE_MAX_LENGTH)
message: str = Field(..., min_length=1, max_length=MESSAGE_HARD_CEILING)
customer_id: str | None = Field(
None, description="目标客户:customer 角色忽略(强制本人);advisor/risk/analyst 可指定(过归属校验)"
)
@@ -98,6 +106,35 @@ def chat_api(req: ChatRequest, request: Request, auth: AuthContext = Depends(get
if not message:
raise ApiError(400, "BAD_REQUEST", "message is blank")
# T-03 输入防护(F-03/G-03):准入后、会话解析前 fail-fast——被拒输入
# 不建会话、不落消息表。命中即拒(拍板:宁可误拒不可漏放);留痕失败
# 降级 warning,拒绝语义优先(与 deps 401/403 留痕降级同口径)。
verdict = input_guard.inspect_message(message)
if verdict.blocked:
try:
_repo().insert_input_guard_log(
trace_id=current_trace() or new_trace(),
agent_type=agent_type,
actor_id=auth.actor_id,
guard_type=verdict.guard_type or input_guard.GUARD_INJECTION,
action="blocked",
raw_excerpt=message[:1024],
session_id=req.session_id,
)
except Exception:
logger.warning(
"input guard log failed (degraded): actor=%s type=%s",
auth.actor_id,
verdict.guard_type,
exc_info=True,
)
code = (
"GUARD_BLOCKED_OVERSIZE"
if verdict.guard_type == input_guard.GUARD_OVERSIZE
else "GUARD_BLOCKED_INJECTION"
)
raise ApiError(400, code, "message rejected by input guard")
customer_id = _resolve_customer_id(auth, agent_type, req.customer_id)
session_repo = _session_repo()