Files

198 lines
14 KiB
Markdown
Raw Permalink Normal View History

# 风控 Agent 模块边界与合并接缝标注
> 版本:v1.0 · 2026-09-07 18:20
> 适用分支:`risk-control-agent`(当前唯一开发分支)
> 定位:**给 AL-09 合并 main 用的裁决依据**,也供日常改码判断"这个文件动了会不会影响宿主"。
> 关联:《修改报告-对齐main基准.md》(阶段一执行依据)、《实现方案-风控追加需求v1.1-C4C6.md》(阶段二编码依据)
---
## 0. 一句话结论
**风控 Agent 按"独立封装模块"自治:模块内部(含自有鉴权)自己说了算,与宿主的耦合收敛到 4 个接缝;合并 main 时不逐文件融合,只在接缝处接线。**
采用本方案后,main 侧 `gateway/*`、`middleware/trace.py`、`utils/input_guard.py` 等文件与模块私有实现**允许并存**——它们不是"重复实现",而是"宿主层"与"模块层"各自的实现,只要接缝不串、模块不被宿主覆盖即可。
**前提(三条,缺一不可)**:
1. 模块私有文件不被宿主同名文件覆盖(合并时以模块版本为准);
2. 模块对宿主的依赖只走 §2 的 4 个接缝,不得直接 import 宿主私有实现;
3. 接缝由 `tests/test_module_boundary.py` 锁死,宿主一改破坏契约即红灯。
---
## 1. 模块边界(文件归属表)
### 1.1 A 类 · 模块私有(风控业务,合并时全量保留,不与 main 融合)
| 路径 | 说明 |
| --- | --- |
| `app/service/risk/` | 风控引擎、规则、预警服务、对话 Tool、L3 画像、AML(阶段 A/B/C 全部成果) |
| `app/service/suitability.py` | 适当性服务(AL-05 换核后为 main 契约 + 我方兼容层) |
| `app/service/tool_service.py` | Tool 编排与文案汇总 |
| `app/repository/risk_repository.py` | 风控读写仓储 |
| `app/repository/core_ro.py` | Core 只读(AL-03 已与 main 融合,归模块私有) |
| `app/repository/session_repository.py` | 会话仓储(模块自用) |
| `app/api/risk.py`、`app/api/simulate.py` | 风控 4 API + 交易网关模拟入口 |
| `app/gateway/trade_gateway.py` | 交易网关(C6 需透传 actor_id) |
| `app/model/suitability.py` | 适当性落库行构造(AL-04 引入,与 main 同名 → **冲突时以模块版为准**) |
| `scripts/demo/*`、`scripts/core/*` | 演示与 Core 种子脚本 |
| `tests/`(除 test_module_boundary 外) | 模块测试;**AL-09 当时 530 passed** · **当前全量基线见 MEMORY §0** |
### 1.2 B 类 · 模块私有基建(与宿主同类但模块内自用,**允许与 main 并存**)
| 模块侧(我方) | 宿主侧(main) | 并存是否安全 | 说明 |
| --- | --- | --- | --- |
| `app/service/auth_service.py` + `app/api/deps.py` | `app/gateway/{jwt_service,auth_deps,rbac,ownership}.py` | ✅ 安全 | 模块内 API 一律 `Depends(get_auth_context)` 走 deps.py,不碰 gateway |
| `app/utils/trace.py` + `app/api/audit_middleware.py` | `app/middleware/trace.py` | ✅ 安全 | 模块用 contextvar 取 trace;宿主用 TraceMiddleware,互不读写 |
| `app/service/input_guard.py` | `app/utils/input_guard.py` | ✅ 安全 | 模块版含 42 词表 + oversize 4000 + Redis 限流 + 留痕;宿主版仅 SQL 注入校验 |
| `app/utils/db.py`(`get_engine(db)`) | `app/config/database.py`(`get_agent_engine/get_core_engine`) | ⚠️ 有条件 | 二者都是双库,功能等价;但**连接池分裂**——模块 Side 不得 import `config.database`,见 §2-S4 |
> ⚠️ 唯一实质风险:B 类第 4 行。其余三行只要"各用各的"就不冲突。
### 1.3 C 类 · 公共层/接缝(合并时以 main 为主,模块做适配)
| 接缝 | 文件 | 处置 |
| --- | --- | --- |
| S1 | `app/main.py` | 以 main 骨架为主,补挂模块 router 与中间件(见 §2-S1) |
| S2 | `app/config/settings.py` | 以 main 为主 + **必须保留模块私有字段**(见 §2-S3) |
| S3 | `app/utils/exceptions.py`、`app/utils/response.py` | 以 main 为主,补齐模块用到的错误码与响应字段 |
| S4 | `app/service/agent_service.py`、`app/service/memory_service.py` | 以 main 为主(chat 主链路),模块 Tool 通过注册表挂载 |
### 1.4 D 类 · main 独有新增(合并时静默并入,模块不依赖)
`app/api/auth.py`、`app/gateway/{auth_deps,jwt_service,ownership,rbac}.py`、`app/middleware/{__init__,trace}.py`、
`app/repository/{advisor_rel,agent,audit}_repository.py`、`app/utils/input_guard.py`、`app/model/schemas.py`、
`app/config/database.py`、`tests/test_wave0_*.py`
处置:**全部接纳,不删除**(它们是宿主 Wave 0 的正当实现),但模块代码**不得 import** 其中任何一个——由 §3 防呆测试锁死。
---
## 2. 四个接缝(合并时唯一需要人工接线的地方)
### S1 · 挂载点(`app/main.py`)
我方现状:挂 `risk` / `simulate` / `chat` 三个 router;两层 `@app.middleware("http")`(audit + trace)。
main 现状:`add_middleware(TraceMiddleware)` + 挂 `auth` / `chat`。
合并口径:
```python
# 宿主部分保留 main 写法
app.add_middleware(TraceMiddleware)
app.include_router(auth_router)
app.include_router(chat_router)
# 模块部分:追加挂载,中间件改为模块内注册(见下)
app.include_router(risk_router)
app.include_router(simulate_router)
setup_risk_module(app) # AL-09 已完成:注册 audit + trace 两层 http 中间件
```
**注意**:main 用 `add_middleware`,我方用 `@app.middleware("http")`——两种注册方式可共存,但中间件执行顺序需实测确认(trace 必须在 audit 内层,保证 audit 能读到 trace_id)。这列为 AL-09 必测项。
### S2 · AuthContext 契约(**最关键的接缝**)
| 字段/方法 | 模块侧(deps.AuthContext) | 宿主侧(schemas.AuthContext) | 差异后果 |
| --- | --- | --- | --- |
| 主体标识 | `actor_id` | `sub` | 字段名不同,全量 `auth.actor_id` 引用会 AttributeError |
| trace | 无(走 contextvar `current_trace`) | `trace_id`(必填) | 模块需从 contextvar 或适配器补齐 |
| agent_type | 无(单独传参) | `agent_type`(必填,4 值) | 取值集合**双方一致**:`customer/advisor/analyst/risk` ✅ |
| `has_role` | `has_role(*roles)` 多参 | `has_role(role)` 单参 | 多参调用会 TypeError |
| 权限判断 | `has_permission(perm)` | `has_perm(perm)`(支持 `xx:*` 通配) | 方法名不同 |
| 其余 | `roles/customer_id/token_type/permissions/tenant_id/jti` | 同左(tenant_id 默认 `default`,jti 非空) | 基本对齐 |
**处置:写适配器,不改模块内部。** 已预制 `app/api/auth_adapter.py`(当前未接线,AL-09 时接入):
```python
from app.api.auth_adapter import from_host_auth
# 宿主 AuthContext(sub/trace_id/agent_type) → 模块 AuthContext(actor_id/...)
ctx = from_host_auth(host_ctx)
```
适配器保证 `sub→actor_id`、`trace_id→contextvar`、`has_role` 多参语义、权限前缀通配到 `permissions` 的展开。
### S3 · settings 字段
- **main 新增需吸收**:`jwt_dev_algorithm`(HS256)、`jwt_dev_expire_hours`(8)。`jwt_dev_secret` 双方同名同值,无冲突。
- **模块私有,合并时一个都不能丢**:
- 双库:`mysql_core_database`
- 中间件:`redis_url`
- 知识库:`milvus_uri`、`ollama_base_url`、`embed_model`、`embed_dim`、`embed_timeout_seconds`
- LLM:`deepseek_api_key`、`deepseek_base_url`
- 风控阈值 11 项:`risk_large_amount`、`risk_daily_total`、`risk_freq_count`、`risk_probe_window_minutes`、`risk_probe_count`、`risk_probe_amount`、`risk_small_amount`、`risk_small_count`、`risk_aml_default_threshold`、`guard_rate_limit_max`、`guard_rate_limit_window_seconds`
- **⚠️ issuer 硬伤**:模块 `jwt_issuer = https://idp.jinrong.internal`,宿主 `https://idp.jinrong.dev`。
自治方案下**不改任何一方**,由 S2 适配器在模块入口完成映射;已签发 token 不受影响(模块继续认自己的 issuer)。
### S4 · 引擎工厂
模块统一用 `app/utils/db.py:get_engine(database)`(含 `dispose_engines` 生命周期)。
宿主 `app/config/database.py` 提供 `get_agent_engine()` / `get_core_engine()`。
**红线**:模块代码一律不得 `from app.config.database import ...`,否则连接池分裂、测试 monkeypatch 失效。由防呆测试锁死。
AL-09 可选优化:让 `config.database` 内部转调 `utils.db.get_engine`,保留宿主 API 外观、共用一处连接池。
---
## 3. 防呆机制(`tests/test_module_boundary.py`)
新增 4 组断言,任何一条被破坏即测试失败:
1. **模块私有文件存在性**:A 类关键文件不得被误删。
2. **禁止跨层 import**:模块文件(A/B 类)中不得出现 `from app.gateway.auth_deps`、`from app.config.database`、`from app.middleware.trace`、`from app.utils.input_guard`(宿主版)的导入。
3. **AuthContext 契约**:模块 `AuthContext` 必须保留 `actor_id/roles/customer_id/token_type/permissions/tenant_id/jti` 字段与 `has_role(多参)/has_permission/is_customer` 方法——防合并时被宿主类替换。
4. **settings 私有字段**:§2-S3 列出的模块私有配置必须齐全;`AGENT_TYPES` 必须等于 `("customer","advisor","analyst","risk")`。
> 用法:AL-09 合并后立刻跑 `pytest tests/test_module_boundary.py`,全绿才说明模块没被宿主侵蚀。
---
## 4. 冲突裁决表(AL-09 用,试合并实测 20 个冲突文件)
> 实测命令:`git merge-tree --write-tree --name-only risk-control-agent 3995cb4`(只读,可随时复算)
| # | 冲突文件 | 归属 | 裁决 |
| --- | --- | --- | --- |
| 1 | `app/api/chat.py` | C 公共 | 以 main 为主;模块对话能力经 Tool 注册表与 `agent_service` 挂载,不占用此文件 |
| 2 | `app/main.py` | C 接缝 S1 | 以 main 骨架为主 + 追加模块 router/中间件(见 §2-S1) |
| 3 | `app/config/settings.py` | C 接缝 S3 | 以 main 为主 + 保留模块私有字段(见 §2-S3 清单) |
| 4 | `app/service/agent_service.py` | C 公共 | 以 main 为主;注意入口函数差异:main `run_chat(ctx,msg,cust)->(str,bool)` vs 模块 `chat(...)`,**由适配层统一,勿直接替换** |
| 5 | `app/service/memory_service.py` | C 公共 | 以 main 为主,补齐模块会话窗口所需方法 |
| 6 | `app/utils/exceptions.py` | C 公共 | 以 main 为主,补齐模块错误码(`AUTH_*`、`STATE_CONFLICT` 等) |
| 7 | `app/utils/response.py` | C 公共 | 以 main 为主,补齐模块响应字段 |
| 8 | `app/repository/core_ro.py` | A 私有 | **以模块版为准**(AL-03 已融合 main 四项 + 我方风控扩展 5 项,main 版缺风控扩展) |
| 9 | `app/model/suitability.py` | A 私有 | **以模块版为准**(AL-04 引入;main 版为纯落库映射,缺模块兼容层) |
| 10 | `app/gateway/__init__.py` | B/D 混合 | 合并两侧导出:保留模块 `trade_gateway` 导出,不引入 main 的 gateway 鉴权导出 |
| 11 | `AGENTS.md` | 文档 | 以我方为准(含 memory 六件套指引),吸收 main 新增条目 |
| 12~18 | `docs/memory/{MEMORY,TODO,REQUIREMENTS,FRAMEWORK,FLOW,ENVIRONMENT,ITERATION}.md` | 文档 | **以我方为准**(六件套是我方进度事实源),逐份吸收 main 新增实质内容 |
| 19 | `scripts/core/02-seed-base.sql` | A 私有 | 手工融合:main 账号(STAFF-10086~50001 等)+ 模块独有 **STAFF-90001**(风控演示账号,main 无!)+ C5 前置 **STAFF-31001/31002** |
| 20 | `tests/conftest.py` | A 私有 | **以模块版为准**(502 用例依赖),再把 main 的 wave0 fixture 增量并入 |
**另有 13 个 main 独有文件不报冲突、静默并入** → 见 §1.4,全部接纳但模块不得 import。
---
## 5. 三处硬伤在自治方案下的处置
| 硬伤 | 原风险 | 自治方案下处置 |
| --- | --- | --- |
| JWT issuer 不一致(`idp.jinrong.internal` vs `.dev`) | 统一后旧 token 全 401 | **AL-09 已统一**:`/api/auth/login` → `issue_dev_token`;`gateway/jwt_service` issuer/audience 读 `settings`;旧 dev token 需重签 |
| main 无 STAFF-90001(风控演示账号) | 演示账号变 401 | 模块账号由模块 seed 维护(`02-seed-base.sql` 融合时保留),不走宿主 `DEFAULT_ROLES_BY_ACTOR` |
| main `infer_roles` 未知 actor 默认 `["analyst"]`(fail-open,含 `sql:execute:readonly`) | 越权风险 | 模块鉴权不采用 `infer_roles`;**若宿主侧修复前上线,须在 AL-09 记录为待办 P1**——模块自身为 fail-closed(缺失 X-Agent-Type 即 401) |
---
## 6. AL-09 执行步骤(按本方案修订)
1. `git merge main`(预期 20 冲突,按 §4 表逐个裁决,禁止 `git checkout --ours/theirs` 批量解决);
2. 跑 `pytest tests/test_module_boundary.py`,红了先修边界再继续;
3. 接 S2 适配器:`app/api/auth_adapter.py` 接线到模块入口;
4. 接 S1 挂载点:`main.py` 补 router + 两层中间件,实测中间件顺序(trace 在内层);
5. 处理 S4:确认模块无 `config.database` 导入(防呆测试已覆盖);
6. 融合 `02-seed-base.sql`(保留 STAFF-90001 / 补 STAFF-31001、31002);
7. SOP §2 重灌演示库 → 全量 `pytest` 须 **530 passed 0 skipped** + 边界用例全绿;
8. uvicorn 冒烟:三端点 + 模块 4 API 逐个验证(注意 JWT 与 debug 头两条通道都要走一遍)。
---
## 7. 待办与遗留
- [x] AL-09 实现 `setup_risk_module(app)` 与中间件注册(2026-09-08)
- [x] 中间件执行顺序实测;trace 对 ApiError 等 re-raise(2026-09-08)
- [ ] 宿主 `infer_roles` fail-open 缺陷:建议宿主侧修复(P1,非模块阻塞项)
- [ ] 模块代码若未来要彻底隔离,可迁至 `app/modules/risk/`——本次**不做**(属重构,收益 < 风险)