模块 1 · 双栈鉴权

JWT 双栈:
模块鉴权(deps.py)与宿主网关鉴权(gateway/auth_deps.py)各管一线

模块 API 走模块鉴权(app/api/deps.py)(get_auth_context / get_platform_auth_context); 宿主 Wave 0 网关走宿主网关鉴权(app/gateway/auth_deps.py)。 模块禁止 import gateway——指挥 AI 改鉴权时,先确认改的是哪条栈。

两套鉴权表(背下来少踩坑)

通道入口函数X-Agent-Type典型路由
Agent / 对话get_auth_contextJWT 通道必填 + 准入矩阵/api/chat, /api/risk/*, simulate
平台只读get_platform_auth_context不要/api/customers/*, /api/analyst/*
宿主网关宿主网关鉴权(gateway/auth_deps.py)· get_auth_context宿主口径Wave 0 四件套(模块不 import)
debug 头兜底: 仅 APP_ENV=development 且无 RS256 公钥时,可用 X-Debug-Role + X-Debug-Actor 冒充身份——pytest 和部分演示 SOP 依赖此通道;生产一律 Bearer JWT。
模块鉴权(deps.py)· 对话线
agent_type = request.headers.get("X-Agent-Type")
if not agent_type:
    raise ApiError(401, "AUTH_401_MISSING_AGENT_TYPE")
assert_agent_access(auth, agent_type, ...)

# 平台线 — 无上述检查
def get_platform_auth_context(request):
    """不要求 X-Agent-Type"""
白话

对话线:验完 JWT 还要读 Agent 头,并对照 AGENT_ACCESS_MATRIX。

例如客户 token 不能带 X-Agent-Type: risk,否则 403。

平台线:验 JWT 就放行到归属断言,不问你走哪条 Agent。

宿主网关鉴权(gateway/auth_deps.py)栈是宿主合并用,和模块鉴权(deps.py)双栈并存,别在模块里混 import。

准入矩阵速览

customer仅 customer 角色
advisoradvisor / compliance / ops
analystanalyst / compliance
riskrisk_officer / risk_manager / service_risk

customers.py 的 Depends 应该从哪 import?

模块 2 · 会话记忆

session_repository + Redis 会话窗口(memory_service.py):
Redis 窗口,MySQL 权威

对话每轮消息同步写 MySQL(权威),同时用 Redis 缓存最近 N≤20 条,TTL 2 小时。读历史时 Redis 命中快,miss 则回源 session_repository。

数据流:用户发第二条消息

🚪对话 HTTP 入口(chat.py)
🗄session_repository
⚡Redis 会话窗口(memory_service.py)

Key 格式:sess:{agent}:{session_id}:msgs

降级策略: Redis 写失败只打 warning,不阻塞对话;读失败回源 MySQL。指挥 AI 加缓存时别破坏「MySQL 为准」这条铁律。
Redis 会话窗口(memory_service.py)
def window_key(agent_type, session_id):
    return f"sess:{agent_type}:{session_id}:msgs"

def get_recent(agent_type, session_id, limit=20):
    raw = get_gateway().lrange(window_key(...), -limit, -1)
    if raw: return [json.loads(item) for item in raw]
    return SessionRepository().list_messages(session_id, limit=limit)

def append_window(...):
    client.rpush(key, ...); client.ltrim(key, -20, -1); client.expire(key, 7200)
白话

每个会话在 Redis 里是一条 List,key 带 agent 类型防串线。

读:先捞 List 尾部 N 条;空或报错就去 MySQL 查 agent_message 表。

写:新消息 RPUSH 进去,LTRIM 砍掉太老的,EXPIRE 两小时自动过期。

session_repository 管表结构、分页列表、关闭会话——和合规审计表分开。

Redis 里 sess:customer:xxx:msgs 过期了,历史消息去哪了?

模块 3 · 防护与审计

input_guard 限流注入、
审计中间件(audit_middleware.py)留痕

用户消息进 LLM 前要经过 input_guard; 每个 HTTP 请求经过 审计中间件(audit_middleware.py) 写访问审计。鉴权 403 还会双写 audit_log + input_guard_log(平台线部分跳过 ENUM 限制)。

input_guard 三类检查

prompt_injection

黑名单短语(「忽略上文」「system prompt」等)→ 拒答 + 落库 guard_type=injection

oversize

业务上限 4000 字(guard 层判定才能留痕;Pydantic 硬顶只防 DoS)

rate_limit

check_rate_limit(agent_type, actor_id) 超限 → AUTH 类拒绝 + 限流日志

群聊:恶意输入被拦下

对话 HTTP 入口(chat.py)+ 审计中间件(audit_middleware.py)
if not input_guard.check_rate_limit(agent_type, auth.actor_id):
    insert_input_guard_log(..., guard_type=GUARD_RATE_LIMIT)
    raise ApiError(...)

verdict = input_guard.inspect_message(message)
if verdict.reject:
    insert_input_guard_log(..., guard_type=verdict.guard_type)

# 应用入口(main.py)挂载
async def audit_middleware(request, call_next):
    # INSERT audit_log 单请求访问记录
白话

先发消息前查频率:同 actor 刷太快就拒,并记限流日志。

再扫内容:注入/超长命中就不调 LLM,直接拒答+留痕。

审计中间件(audit_middleware.py)包在更外层:每个 HTTP 进来都记一条访问审计。

模块鉴权(deps.py).deny 鉴权失败也会双写——平台 agent_type 时 input_guard_log 可能跳过 ENUM 限制。

用户粘贴 5000 字,要在哪一层拦截才能落 input_guard_log?

审计中间件(audit_middleware.py)和 input_guard 的关系?

模块 4 · 生产认知

审计只 INSERT
Redis 6380 与 fail-open

共用底座课讲了双栈鉴权与 input_guard,这模块补生产环境行为和画像/cache 接缝—— 指挥 AI 改「吊销 fail-closed」或「审计可改」前必看。

G-02:审计表只 INSERT

audit_log · input_guard_log · risk_suitability_log 等合规留痕表禁止 UPDATE/DELETE。任何「改历史审计」需求都应被拒绝,改为追加更正记录。

input_guard 命中: fail-fast,不建会话——和 LangGraph 内 fallback 不同,没有 reply 回合入库。

fail-open 与 debug 双闸门

①

Redis 会话窗口 fail-open — Redis 挂了对话仍能回,只是少短期记忆(客服线同样策略)。

②

jti 吊销 fail-open — 吊销服务异常时 token 仍可能通过;生产要监控 Redis。

③

debug 头 — 仅 APP_ENV=development 且无 RS256 公钥;生产有公钥时 debug 头失效。

④

Redis 6380 + RESP2 — Docker 映射 6380;Windows 本机 6379 旧 Redis 3 不兼容。

L1/L2 热缓存:谁接了谁没接

接缝快照
客服 profile_maybe_extract → L1 抽槽 ✓
ProfileHotCache → 部分读 ✓
L2 Repository / 全量 Redis L1 → 开放项
宿主 POST /api/chat 非 customer → 未全接 L1 热缓存
白话

指挥 AI「全 Agent 统一画像 Redis」前,先对 FRAMEWORK 实现状态表。

Bearer 全环境优先——pytest 仍用 dev token,但生产必须 RS256 + 禁 debug 头冒充。

运维说「把 audit_log 里错的那条改成正确 decision」,你怎么回?