## 对 ZSY 三件事的核实与裁决(docs/33-ZSY底座扩展确认-回复.md) 1. **访客身份**:`data_scope="public"` 我扫了全平台 26 个消费点,对未知值的行为一致 fail closed(6 处 `== "all"` 判假、1 处 `!= "all"` 会要求 customer_ids 非空否则 403、 1 处显式白名单直接拒),**安全,不用改**。但有一条硬约束必须先确认:全平台有 **72 处 `int(context.user_id)`,只有 3 处做了防御**——若访客的 sub 不是纯数字, 其中"权限被拒时要写审计"的路径会把本该 403 的情况变成 500。已要求他确认 sub 为纯十进制数字、且 visitor 分支复用同一套 `jwt.decode`(含 isdecimal 校验), 而不是另写第二个鉴权入口。 2. **客服 Agent**:`allowed_roles` 扩集合与确定性安全路由可接受;`recalls_customer_memory` 是新声明字段,已要求**默认值必须是 True**(否则会静默关掉所有既有 Agent 的记忆召回, 界面看不出来、只表现为回答变差)。品牌名(南方科技 → 奶龙基金责任有限公司)属业务 口径,已上报项目方定,不由技术侧拍板。 3. **current_customer_id**:**他的判断正确**。我独立实测 information_schema: `EXTRA=''` 且 `GENERATION_EXPRESSION=''`,配合 baseline_generated.sql 无 GENERATED 子句、seed_profile_demo.py 的注释、profile_repository.py 用原生 SQL 显式写入, 四处一致 ⇒ `docs/00` 第 783 行"生成列"的描述是错的。处置:按真实 schema 映射成 普通可空列、**不改 docs/00**(规则 1)、**不补迁移**(DDL 本身正确,为让文档成真而加 生成列会改变既有列语义,违反规则 4)、但**必须登记这处偏差**。 另:他删了两个死代码文件(含一处硬编码 Milvus 字段名,违反 AGENTS.md §E),方向认同, 但要求他在 PR 里附上两条 git grep 的实际输出以证明"全仓唯一引用是它自己的单测"。 ## 落实我在回复里承诺的两件事 - `docs/08-数据库结构审计基线.md` 新增"六、已知文档偏差",逐条登记上述偏差(含四处 证据与处置口径),并注明发现方式; - `AGENTS.md` 环境口径新增 `MILVUS_LOCAL_URI` 的坑:配了会让健康检查与部分检索指向 本地 Milvus Lite 文件,出现"健康检查正常、实际查的是另一个库";并注明 `milvus-lite` 属本地开发依赖,应放 `optional-dependencies` 而非主依赖。 ## 顺带更新 AGENTS.md 的过期口径 - 表数 68 → **89**(场内 51 + 场外/推广 17 + 投顾 21),并注明投顾那 21 张的登记文档待补; - 测试基线从"1034 passed / 1 failed"改为实测值:ruff 干净 / mypy 228 文件 0 错 / 1207 passed 0 failed / integration 99 passed,并指向 docs/32; - mypy 那条从"本机 181 错、双方不可比"改成可操作的判据:先对版本,根因是某一侧虚拟环境 没满足 pyproject 的 sqlalchemy>=2.0,<3 / mypy>=1.14,<2;并写明**不要装 sqlalchemy2-stubs** (那是给 1.4 的,2.0 自带 py.typed,装了反而按 1.4 API 报一批新错)。 文档守卫:42 份无编号冲突。
10 KiB
回复 ZSY:底座扩展确认(v1)
致:ZSY 被回复:
致qyqy_底座扩展确认沟通文案_v1.md我方基线:qyqy_develop(含你合并的c4a73b7之后的平台侧改动) 结论:三件事方向都可以接受,但第 1 件有一条硬约束必须先确认(见 §1.2); 第 3 件的判断你是对的,docs/00那处描述确实错了,处置方式见 §3。
1. 访客身份(security.py + auth.py)
1.1 data_scope="public" 这个新值,我替你核过了:安全
我扫了全平台 data_scope 的全部 26 个消费点,对未知值的行为是一致 fail closed 的:
| 写法 | 处数 | "public" 的结果 |
|---|---|---|
== "all" |
6 | 判假 ⇒ 走保守分支 |
!= "all" |
1(risk_analysis_service.py:126) |
进入分支 ⇒ 要求 customer_ids 非空,否则 ForbiddenAgentError |
显式白名单 {"self","own_customers","all"} |
1(financial_nl2sql_service.py:286) |
不在集合内 ⇒ 直接拒绝 |
访客的 customer_ids 本来就是空的,所以 != "all" 那一支会先拒后查。
结论:新增 public 不会意外放行任何数据,不用改这 26 处。
顺带说明它的实际作用:访客要的是"查公开知识",那条路径走 knowledge:query权限、
不读 data_scope,所以 public 目前主要是语义标注——这没问题,标注清楚比复用 self 更好。
1.2 ⚠️ 必须先确认:访客令牌的 sub 必须是纯数字
这是全平台最硬的一条约束,请务必确认你的实现满足:
# app/core/security.py:57-61 —— 现有校验
subject = claims["sub"]
if (not isinstance(subject, str) or not subject.isascii()
or not subject.isdecimal() or len(subject) > 20
or not 0 < int(subject) <= 18446744073709551615):
raise UnauthorizedAgentError("invalid subject")
即使这个校验通过了,平台里仍有 72 处 int(context.user_id),而只有 3 处做了防御:
有防御:offsite_fund_service.py:2458 / offsite_nl2sql_adapter.py:68 / suitability_service.py:250
(写法都是 int(...) if context.user_id.isdigit()/isdecimal() else None)
没有防御:其余 69 处,例如
authorization_service.py:21 actor_id=int(context.user_id) ← 一旦发生权限拒绝就要写这条审计
tool_executor.py:138 actor_id=int(context.user_id)
identity_repository.py:18 int(identity.user_id)
conversation_service.py:36/60/63/68/87
public_platform_service.py:36/44/77
…(共 69 处)
所以如果访客的 sub 是 visitor-<uuid> 这类非数字,任何一次命中就会抛 ValueError——
而且其中好几处是"权限被拒时要写审计"的路径,表现为本该 403 的地方变成 500。
请你确认两点:
- 访客令牌的
sub是纯十进制数字(例如保留 id0,或某个不与sys_user冲突的号段); visitor分支复用了同一个jwt.decode(含algorithms/iss/aud/exp/nbf/jti以及上面那段sub校验),只是跳过IdentityService.resolve()——而不是自己另写一套。 另写一套等于开了第二个鉴权入口,这与"单点鉴权"的约定冲突。
如果
sub只能是字符串,那正确做法不是改成访客专用校验,而是把那 69 处int(context.user_id)一并收敛成app/core/里的一个转换函数(非数字返回None)。 那是一次跨模块改动,建议单开一轮做,不要混在访客功能里。
1.3 两条小约束
- 权限集保持最小:
("agent:run", "knowledge:query")看起来正好,请不要顺手加knowledge:reference:read(那是读"知识引用令牌"用的,访客不需要)。 - 访客不产生客户侧副作用:
recalls_customer_memory=False走对了方向; 另外确认访客不写interaction_audit.target_customer_id(留None)。
2. 客服 Agent 的角色与行为
2.1 allowed_roles 扩为 ("visitor","customer") —— 可以
访客要用客服 Agent,这是必需的。AgentDefinition.allowed_roles 本来就是声明式的,
扩集合不改语义。
2.2 recalls_customer_memory=False —— 可以,但默认值必须是 True
这是新增声明字段,现有代码里没有它(我 grep 过 app/service/agent/,只有 recall_memory
这个 base 方法)。请务必保证:
默认值 = True(即"照常召回"),只有显式声明 False 的 Agent 才跳过
否则一旦默认成 False,所有既有 Agent(客服、风控、投顾、demo)的记忆召回会被静默关掉——
而它在界面上看不出来,只会表现为"回答变差"。
2.3 handle() 里插入确定性安全路由 —— 可以,但请补测试
"安全/合规/账户/人工优先于检索"这个顺序是对的(客服那边原本就有类似的
is_profile_question() 确定性前置)。请为每条分支补一个用例,锁住"优先级顺序"本身——
这类路由最容易在后续合并里被悄悄挪位置。
2.4 品牌名 —— 这条我不能定,需要项目方拍板
COMPANY 现在确实是 "南方科技"(customer_service.py:153)。但你改成的
「奶龙基金责任有限公司」是风控那条线的品牌(docs/风控业务演示文档/09-奶龙风控智能助手说明.md)。
所以这不是"改个常量",而是两个业务线是否共用一个品牌主体的问题。我倾向统一(同一家公司 的客服和风控不该是两个主体),但这属于业务口径,我已把它单独列给项目方定, 定了我立刻同步。
3. current_customer_id —— 你的判断是对的,docs/00 那处描述错了
我独立核了一遍,你的证据全部成立,而且我这里也有了直接证据:
information_schema.COLUMNS(本机实测):
COLUMN_NAME = current_customer_id
COLUMN_TYPE = bigint unsigned
IS_NULLABLE = YES
EXTRA = "" ← 不是生成列、也不是自增
GENERATION_EXPRESSION = "" ← 没有生成表达式
与 alembic/baseline_generated.sql 没有 GENERATED 子句、tools/seed_profile_demo.py
的注释、profile_repository.py 用原生 SQL 显式写入——四处一致。
处置(我的裁定):
- 按真实 schema 映射成普通可空列 —— 你已经这么做了,对;
docs/00不改 —— 规则 1 明确它是不可变业务基线,哪怕某处描述有误也不由我们改;- 但要把这处偏差登记下来,否则第 5 个人还会再踩一次。我会在
docs/08-数据库结构审计基线.md(审计口径文档)里记一条"已知文档偏差"; - 不建议补迁移:DDL 本来就是对的(普通可空列),错的只是文档描述。为"让文档变成真的" 去加一条生成列迁移,会改变既有列语义(规则 4)。
- 同一张表两个声明类(
risk_questionnaire.py内联 +profile.py)会让 SQLAlchemy 直接拒绝导入模型——这个必须去重,你做的方向对;请统一从profile.py导出。
4. 顺带三件小事
| 项 | 我的意见 |
|---|---|
query_knowledge 与 search_knowledge 并存 |
可以保留(都指向同一 handler、实际范围由 config_release 白名单收口),但请在 docs/05 §8.4 或你的接入说明里明确写成"别名",否则下一个人会以为平台有两个检索工具 |
.gitignore 加 .worktrees/ 和 data/milvus/ |
可以。顺带提醒:.workdir/ 也已在里面(NL 那边的 conftest 用它落测试临时目录) |
依赖新增 milvus-lite |
请放进 pyproject.toml 的 optional-dependencies,不要进主 dependencies。它只是本地开发用,进主依赖会让生产环境多背一个包,而且 MILVUS_LOCAL_URI 那条坑正是它引入的 |
你提的 MILVUS_LOCAL_URI 那个坑很有价值:一旦在 .env 里配了,健康检查会指向本地
Milvus Lite 文件,出现"健康检查正常、实际查的是另一个库"。建议把它写进 AGENTS.md 的
环境口径一节(和 config_release、Milvus schema 那几条并列)——我来加,你确认措辞即可。
5. 关于你删掉的两个文件
app/service/knowledge_tool_service.py 与 app/infrastructure/milvus_knowledge_adapter.py,
你判断是生产死代码、且后者硬编码了 Milvus 字段名(违反 AGENTS.md §E)——方向我认同,
硬编码字段名那条尤其该删(另一套环境会被打挂)。
但我这边看不到你的分支(ZSY_develop 未推送),所以无法独立核实"全仓唯一引用是它自己的单测"。
请在 PR 描述里附上两条 grep 的实际输出,例如:
git grep -n "knowledge_tool_service" -- app/ tests/ tools/
git grep -n "milvus_knowledge_adapter" -- app/ tests/ tools/
只要输出里只剩它们自身(和各自的单测),我就照批。
6. 你处理得好的两处(照做)
docs/05的§8.5:没有把既有的§8.2/§8.3往后挤,而是新增到末尾,AGENTS.md/docs/09/docs/14里对 §8.3、§8.4 的引用继续成立 —— 这是对的, 编号被复用比编号不够更麻烦(我们这边刚因两家同时占用docs/21、docs/22让过两次号);- 合并前先按底座规则自查、把撞规则的地方清理成独立提交(
9aaacc2, 9 文件 +189/−483)—— 这个习惯请保持,它让评审能按提交看,而不是在一大坨 diff 里找。
7. 结论与下一步
可以推进:§1.3、§2.1、§2.3、§3 的处置、§4、§5(附 grep 证据后)。
需你先补一句确认:§1.2 的 sub 是否为纯数字 + 是否复用同一套 decode 校验;
§2.2 的默认值是否为 True。
需项目方拍板(我已上报,不占你时间):§2.4 的品牌名。
这三条确认后,你推 ZSY_develop 并开 PR,我这边按 docs/32-平台侧交接与联调准备.md §5
的清单合:alembic upgrade head → 表审计 → 文档守卫 → 四项门禁 → 真机联调。
附:我方最近新增的与你这条线相邻的东西,联调时可能用得上 ——
POST /api/v1/auth/tokens(账号密码登录)、GET /api/v1/admin/roles等四个 RBAC 只读接口、tools/login_console.py(浏览器登录测试台)、tools/create_test_user.py(建测试账号)。