第一版写「8 方法 / 314 行」错了一个数量级。错因:闭包只跟 self._x(),而 _compute_consultation_workflow 只有 6 行、转给模块级 execute_consultation_workflow(self, ...),那个函数体 394 行、对传进来的 handler 调 16 个方法再往下扇出。跟丢那一跳,300 行被当成了全部。 执行方重算:约 115 方法 / 4,104 行,占全类 51% 方法;碰 HTTP 上下文的 仍是 0 个——本单成立的前提没变。 产品在 A/B/C 中选定 B(mixin 抽取),并授权把 §4「不得搬 ≥150 行业务 方法」按本意解释:那条红线针对的是 C 那种逐行改写,而 mixin 是把方法体 原样挪家、self 含义不变、靠 MRO 解析,HTTP 那一面零变化。决策记录里已 写明这是该红线的推翻记录。 新增 spike 闸门:搬进 mixin 后 test_api_server_security.py 一字不改直接 跑,绿则继续、红则退回 A(7 方法/300 行、关 2 个伪造点)。已查明两个降 风险事实(security 测试无结构性断言、增长合同用整文件正则),并点名真 风险是循环 import——闭包引用同文件 44 个模块级函数 + 21 个常量/类。 另修正 __new__ 归零口径:合同基线 33 = scripts 4 + tests 29,tests 侧含 不许改的 security 测试,所以归零只针对 scripts 侧 4 → 0。 纯文档推送,不触发门禁、不发布镜像、不部署。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
10 KiB
TASK · 关掉 JyotishAPIHandler.__new__ 后门(mixin 抽取)
- 日期:2026-09-16(2026-09-16 第二版:闭包规模与方案全部改写,见 §1.1)
- 基线 commit:
origin/staging@dc8cae31 - 执行分支:
codex/api-server-backdoor-close-20260916 - 落点:
scripts/jyotish_api_server.py、新建的scripts/consultation_compute_mixin.py(名字可另议)、scripts/consultation_workflow_service.py、scripts/capture_report_blocked_repairs_golden.py、scripts/local_accuracy_report.py、tests/test_api_server_growth_contract.py - 取代
TASK-api-server-decomposition-20260916的阶段 1;该单其余阶段的处置见 §3.3 - 与已合入的 residual-hotspots、home-state-lowering-batch2 无文件重叠
- 规模:约 115 个方法 / 4,104 行整体移进 mixin,方法体一字不改。零行为变化。
1. 第一版的规模前提是错的
第一版写「8 个方法 / 314 行」,错了一个数量级。错因已定位:闭包只跟了 self._x(),而 _compute_consultation_workflow 只有 6 行,转给模块级的 execute_consultation_workflow(self, ...)——那个函数体 394 行、直接对传进来的 handler 调 16 个方法,再往下扇出。跟丢了那一跳,300 行就被当成了全部。
执行方按 §5.1 重算后的实测:
| 数量 | |
|---|---|
| 需要搬的类方法 | 约 115 个 |
| 合计行数 | 约 4,104 行 |
其中碰 HTTP 上下文(self.headers / wfile / rfile / path / client_address) |
0 个 |
占 JyotishAPIHandler(225 方法 / 8,219 行) |
51 % 方法 / 36 % 行 |
闭包按入口干净地劈成两半:
| 部分 | 规模 | 覆盖的伪造点 |
|---|---|---|
| vedastro 证据 + 合盘 | 7 方法 / 300 行 | consultation_workflow_service.py:27、local_accuracy_report.py:141 |
| 咨询工作流 | 114 方法 / 4,086 行 | consultation_workflow_service.py:20、capture_report_blocked_repairs_golden.py:59 |
「0 个碰 HTTP 上下文」这条前提仍然成立,这是本单能做的基础。
2. 事故实证(未变)
handler = JyotishAPIHandler.__new__(JyotishAPIHandler) # 不跑 __init__ 的空壳
__new__ 绕过 BaseHTTPRequestHandler.__init__,造出来的对象没有 headers / wfile / rfile / client_address。今天能跑只是因为被借用的方法碰巧没碰它们——实测 115 个里 0 个碰。
这是一颗类型检查看不见的地雷:任何人往这些方法里加一行 self.headers.get(...),MCP 与两个离线脚本就会在运行时 AttributeError,单元测试与 tsc 都发现不了。
更根本的是依赖方向反了:consultation_workflow_service.py 自称「shared consultation workflow boundary for API and MCP callers」,实际却反过来依赖 HTTP 单体文件。全仓 43 个文件依赖该模块。
3. 决策记录
产品 2026-09-16 在 A / B / C 三案中选定 B(mixin 抽取),并明确:
- §4 的「不得搬 ≥150 行业务方法」按本意解释,mixin 放行。 那条红线的本意是「别做可能悄悄改行为的大重构」,针对的是 C 那种把方法逐行改写成模块级函数。mixin 是把方法体原样挪个家:
self含义不变、方法集合不变、靠 MRO 解析,HTTP 那一面完全没有变化。 执行方不得以「任务书第一版禁止搬 ≥150 行方法」为由拒改——这一条就是那条红线的推翻记录。 - 目标是
__new__在scripts/生产侧归零。 合同测试现行基线NEW_COUNT_BASELINE = 33(scripts 4 + tests 29)。tests/里那 29 处含test_api_server_security.py,而该文件一个字不许改,所以 tests 侧保持不增长即可,归零只针对 scripts 侧 4 → 0。 - 原 decomposition 单的阶段 3(
do_POST78 个 elif 改路由表)仍然不做,由3b17c1b2换好的门禁(类方法数 +__new__计数只许降)长期推进。阶段 2 被本单以 mixin 形式吸收。 - B 必须先过 spike 闸门(见 §5.2)。spike 红了就退回 A(只搬 7 方法 / 300 行、关掉 2 个伪造点),不得硬做。
- 纯搬运,零行为变化。
tests/test_api_server_security.py的 3,841 行断言一条不许改。
4. 硬红线
tests/test_api_server_security.py一条断言不许改。 它红了说明不是纯搬运——回去改实现,不是改断言。这是判断「mixin 有没有搬坏」的主证据。- 方法体一字不改。 mixin 里的方法必须与搬走前逐字相同(除了必要的 import 调整)。允许用
git diff -M/--color-moved之类的手段证明是移动而非改写,并把证明写进进度记录。 - mixin 模块不得 import
jyotish_api_server。 那等于把后门换了个地方开。要有断言钉住这个方向。 - 不得改
do_POST/do_GET的路由结构(阶段 3 不做)。 - 搬完后
JyotishAPIHandler仍须通过继承拥有这些方法——HTTP 侧的可见行为必须零变化。 - 不得顺手升级依赖、不得顺手修不在本单里的 warning。
5. 任务分解
5.1 重算闭包(已完成,结论见 §1)
执行方已按第一版 §5.1 重算并提交在 dfd65528(仅文档提交)。开工时以当时代码复核一次即可,不必从头再算。
5.2 spike 闸门(必须先做,不产出正式交付)
一次性验证,目的是回答「mixin 搬动会不会打红安全测试、会不会解不开循环 import」:
- 把闭包里的方法整体移进
ConsultationComputeMixin,JyotishAPIHandler(BaseHTTPRequestHandler, ConsultationComputeMixin)。 tests/test_api_server_security.py一个字不改直接跑。
判定:
- 绿 → B 可行,继续 5.3。
- 红 → 记录红在哪、为什么,退回 A(只搬 vedastro 证据 + 合盘那 7 方法 / 300 行,关掉 2 个伪造点,scripts 侧 4 → 2),并在进度记录里写明 B 为什么不可行。
已知的两个降风险事实(我查过,可直接用):
test_api_server_security.py里没有任何__dict__/inspect/__qualname__/__module__结构性断言,只按行为断言。- 增长合同数类方法用的是整文件正则
^ (?:async )?def,方法搬到别的文件后计数自然下降,口径不用改。
B 的真风险是循环 import:闭包里的方法引用了同文件的 44 个模块级函数与 21 个模块级常量/类(execute_consultation_workflow、BadRequest、_load_local_module 等)。这些要么跟着搬进 mixin 模块、要么提到第三个共享模块。spike 必须把这一条也验掉——解不开就是 A。
- 验收:spike 结论(绿/红、红在哪)写进进度记录,无论走 B 还是退 A。
5.3 正式搬运
按 spike 验证过的形状做完整搬运。若闭包在两个入口上确实可分(§1 那张表),建议拆成两个 mixin(vedastro 证据 + 合盘 / 咨询工作流),便于将来继续细分;但不强制。
- 验收:
JyotishAPIHandler的方法数显著下降,新值写进合同测试。 - 验收:mixin 模块不 import
jyotish_api_server,有断言。 - 验收:
git diff能证明方法体是移动而非改写。
5.4 四个调用方改成直接用 mixin
consultation_workflow_service.py(两处)、capture_report_blocked_repairs_golden.py、local_accuracy_report.py 改成实例化 mixin,删掉 JyotishAPIHandler.__new__ 与对 jyotish_api_server 的 import。
- 验收:四个文件都不再 import
jyotish_api_server。 - 验收:MCP 侧(
mcp_server.py:741/:4749经由consultation_workflow_service)仍可用——至少一条端到端断言。
5.5 合同测试收基线
tests/test_api_server_growth_contract.py:__new__ 的 scripts 侧计数收到 0(tests 侧保持不增长);类方法数基线更新为收尾实测值;行数粗护栏 baseline 重设为收尾实测 + 300。同步更新文件顶部 docstring 的日期与说明。
- 验收:人为加回一处
JyotishAPIHandler.__new__(在scripts/下)必须让测试变红(贴反向验证)。 - 验收:人为加一个类方法必须变红(贴反向验证)。
- 验收:该文件既有的三条断言(
AGENTS.md必须含must not grow/thinly registered、该测试必须在CORE_PYTEST_TARGETS与快速门里)仍绿。
5.6 记录
本单不产生 Bug 记录(结构改造,不是缺陷),不进 CHANGELOG.md。进度记录里必须有:spike 结论、闭包清单、搬前/搬后的类方法数与行数、两次反向验证结果、方法体未改写的证明。
同轮把原 TASK-api-server-decomposition-20260916 在状态板上标成已取代。
6. 让步顺序
- 5.2 spike 不得砍——它是 B 与 A 的分水岭。
- spike 绿则 5.3 + 5.4 必须一起做(只搬不改调用方,后门还在)。
- spike 红则整单退化为 A,并在进度记录里写清楚,不要硬做 B。
- 5.5、5.6 不得砍。
7. 开工前置命令
git fetch origin --prune
cd .worktrees/api-server-backdoor-close-20260916 # 分支已存在,rebase 到最新 staging
git rebase origin/staging
git status -sb | head -1
grep -rn "JyotishAPIHandler.__new__" --include=*.py . | grep -v "skills/.*/versions/"
grep -cE '^ (async )?def ' scripts/jyotish_api_server.py
wc -l scripts/jyotish_api_server.py
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45 # AGENTS §9
验收命令:
.venv/bin/python -m pytest tests/test_api_server_security.py tests/test_api_server_growth_contract.py \
tests/test_consultation_consumer_context.py tests/test_vedastro_gateway.py \
tests/test_vedastro_snapshot_cache.py
.venv/bin/python scripts/run_quality_gate.py --profile quick # 退出码不要隔着管道取
注意:快速门当前在本机退出 1,唯一原因是无 rsync(staging-backend-workflows.test.ts)与无 Docker 的既有环境缺口,见 BLOCKED.md BLK-002 / BLK-003。pytest 段必须是 0 failed(dc8cae31 之后是 792 passed / 1 skipped)。
8. BUG 编号起点
本单不占 BUG 号。基线 dc8cae31 上最大号 BUG-736。
9. 不在本单范围
- 阶段 3(
do_POST78 个 elif 改路由表)——不立单,由门禁长期推进 execute_consultation_workflow里的任何业务逻辑改动(只能整体搬,不能改)tests/下那 29 处__new__(保持不增长即可)