v19.4.1
审计补丁版 · 鉴权贯通与租户闭环。 不引入新功能,只修「文档说了但代码没做到」的裂缝。
审计方法从「逐行读代码」改为探针实测——对 README 里每一句宣称,写一个最小可运行程序去试着推翻它。结果四条宣称被推翻,逐条修复并写进断言锁死。
🔴 安全与数据权利
鉴权贯通「一道门禁两把钥匙」
修复前两种部署形态都不可用:只设 AIDUMEM_UI_PASSWORD 时未登录 GET /api/facts 返回 200 全裸奔(口令只是前端障眼法);只设 AIDUMEM_API_TOKEN 时前端从不发 Authorization,登录后所有面板 401 报废。根因是认证结果没有服务端载体。现 /login 签发 HttpOnly + SameSite=Lax session cookie,与 Bearer 令牌任一有效即放行;新增 /logout 服务端撤销。
存量零破坏:口令哈希加 source=auto|user 标记——服务首次启动自动生成的口令只守控制台登录,不启用 REST 门禁,既有回环调用方(插件 / MCP / cron)升级后行为不变。
租户闭环
facts 层此前完全没有租户维度(19 处查询无一处过滤),新增 tenant_clause() 覆盖 9 个路由与注入出口,宽松 / 严格双档。同时修掉一处跨租户静默覆盖:唯一约束是 (agent_id, category, fact_key) 而 agent_id 恒写常量,不同租户写同一键位时后写者会直接销毁前者的值。
删除权兑现
级联删除此前清 5 个库却独漏原文保真层,含敏感信息的逐字原文删除后仍可被检索。另外 /search 以 verbatim:<n> 返回原文证据(调用方唯一句柄),但 /delete 不认它——返回成功却什么都没删,而这类原文常无对应 mem0 记忆,遂成可检索但删不掉的孤儿。
移除无 WHERE 全表删
各仓原有 if user_id == "default": DELETE FROM <表>,而 default 正是系统默认租户——清 default 会连带清空所有其他租户。
🟠 功能真伪
幂等键根治:判重键含 recorded_at,而生产载荷(纯字符串)不带时间戳、回落 now(),导致永不撞键——实测同一轮重放 3 次落 3 条。改为稳定因子 (user_id, content_hash, session_id),重复表述累加 occurrences 而非堆行。
中文检索从未走过索引:切词产出 2-gram 而虚拟表是 tokenize='trigram',2 字词元在 trigram 索引里永远匹配不上——每一次中文查询都静默落到 LIKE 全表扫描。20 万条实测稀有中文词 32.8ms → 0.05ms。召回结果新增 _recall_path(fts / like)自证本次真走的哪条路。
observations 幂等建表:该表自 v7 起只有读取方、全仓从无 DDL,全新部署 /observe 直接 500。
4xx 不再被降级成 500:注入拦截的 400 被外层 except Exception 吞掉再包 500,带自动重试的客户端会对着永远会被拒的内容死循环。AST 扫出同一模式 18 处,配源码守卫防复发。
🔍 三个「静默失败自我掩盖」连环案
这三个同型:异常被 except 吞掉后,业务层输出一个语法正确、语义完全错误的「正常结果」。
-
兼容门面缺口致合并任务静默死亡三周。v11.1 重构把显著性能力拆进子包,门面只转发两个钩子,而调用方仍按老接口导入 6 个符号——每天凌晨被 cron 唤起、每次崩在 import 行,日志累积 18 次同样堆栈。期间衰减 / 指标 / 冲突检测 / 技能结晶 / 教训闭环全部未运行,而
/health一直全绿(这些活儿本就不在服务进程里)。 -
两个附属库的级联清理从引入起从未执行。表名与列名双错(
memory_salience真名salience且无user_id列;evolve_snapshots表根本不存在),错误被吞成 debug、计数恒报 0。后果是一条自我掩盖链:留下 252 条幽灵 id → 被当正常记忆持续衰减 → 进入淘汰名单 → 逐个删「早已不存在的东西」→ 日志漂亮报「删除成功 25/25」而向量库分毫未变。 -
技能结晶 SQL 方言错误。
GROUP_CONCAT(DISTINCT x, sep)在 SQLite 报错,异常被吞后输出「生成 0 个候选项」——看似「暂时没发现模式」,实则该 SQL 从未成功执行。
🛡️ 备份纪律与 cron 凭据
备份门禁自相矛盾:原流程「先算校验和 → 再逐个打开库跑完整性检查」,而打开 WAL 库会重建 -shm 并合并日志,当场打废刚算好的基线——create 报通过、require 立刻拒绝。后果比备份失败更坏:硬门禁 100% 拦人,运维只会学会绕过它。改用 SQLite 在线备份接口生成一致快照。
不变量:校验动作本身不得破坏校验基线。
cron 凭据兜底:服务靠 systemd EnvironmentFile 读令牌,但 cron 不加载 .env——门禁一开,定时任务下次触发即集体 401,且失败只写日志无人知晓。新增凭据单一真相源,9 个运维脚本统一复用。
🟡 供应链与加固
- 依赖收敛:
pyproject下限对齐requirements实锁 + 补requires-python = ">=3.10"(此前 pip 安装与源码安装跑两套依赖树,且 3.9 用户不被拦住) - 口令改 PBKDF2-HMAC-SHA256(200k 轮),文件权限 0600,旧哈希首次登录自动升级;改密撤销全部会话
- echarts 落本地(去掉无 SRI 的第三方 CDN 外链,内网 / 离线可用)
/docs/redoc/openapi.json纳入门禁(135 个端点清单等于攻击面地图),AIDUMEM_PUBLIC_DOCS=1可显式放开/stats计数按租户收窄(原为全库计数,可从中推断本机记忆总规模)router_usage(ssh + exec 形态)默认禁用
🟢 文档诚信
「租户硬隔离」改为准确的「按租户收窄可见性」并明示单机自托管定位;README_EN 补齐 Testing & Quality 与 Security Model 两章。
测试数字改为自校验:此前 README 295 / 汇报 328 / 实测 340 三版互相矛盾,根因是数字靠人手抄进文档。新增守卫从 pytest --collect-only 取真值与 README 比对,并校验「通过数 + 跳过数 = 总数」——数字过期会立刻红灯。版本号测试同样从硬编码字面量改为跨文件一致性比对。
质量数据
| 项 | 结果 |
|---|---|
| 测试 | 339 通过 / 12 跳过(完整环境 351 全绿) |
| 新增用例 | 107 项,全部遵循反假绿灯纪律 |
| 覆盖率 | 48% → 51% |
| 编译 | 0 错误(含 4 个 shell 脚本) |
| 生产验收 | 数据集合比对丢失 0 条,服务自崩 0 次 |
本轮沉淀的四条纪律
- 假绿灯:断言必须覆盖生产实际走的代码路径。只测便于构造的形态而绕过真实载荷,绿灯等于没测。
- 宣称即承诺:README 里每一句「硬隔离」「绝不留孤儿」「trigram 索引」都是对全世界的承诺。代码没做到,先改文档。
- 静默失败:干净降级是好纪律,但被吞掉的异常若让业务层输出一个「看起来正常的错误结果」,危害远大于崩溃。每个
except都要能回答「如果这里真失败了,谁会知道」。 - 校验不得破坏基线:任何校验动作都不得改变被校验对象的状态。
安装:pip install aidumei==19.4.1 | Docker:ghcr.io/monkey2jack/aidumei:latest
完整变更:见 CHANGELOG.md