第一版写「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
162 lines
10 KiB
Markdown
162 lines
10 KiB
Markdown
# 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. 事故实证(未变)
|
||
|
||
```python
|
||
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 抽取)**,并明确:
|
||
|
||
1. **§4 的「不得搬 ≥150 行业务方法」按本意解释,mixin 放行。** 那条红线的本意是「别做可能悄悄改行为的大重构」,针对的是 C 那种把方法逐行改写成模块级函数。**mixin 是把方法体原样挪个家:`self` 含义不变、方法集合不变、靠 MRO 解析,HTTP 那一面完全没有变化。** 执行方不得以「任务书第一版禁止搬 ≥150 行方法」为由拒改——这一条就是那条红线的推翻记录。
|
||
2. **目标是 `__new__` 在 `scripts/` 生产侧归零。** 合同测试现行基线 `NEW_COUNT_BASELINE = 33`(scripts 4 + tests 29)。`tests/` 里那 29 处含 `test_api_server_security.py`,而该文件一个字不许改,所以 **tests 侧保持不增长即可,归零只针对 scripts 侧 4 → 0**。
|
||
3. **原 decomposition 单的阶段 3(`do_POST` 78 个 elif 改路由表)仍然不做**,由 `3b17c1b2` 换好的门禁(类方法数 + `__new__` 计数只许降)长期推进。阶段 2 被本单以 mixin 形式吸收。
|
||
4. **B 必须先过 spike 闸门**(见 §5.2)。spike 红了就退回 A(只搬 7 方法 / 300 行、关掉 2 个伪造点),**不得硬做**。
|
||
5. **纯搬运,零行为变化。** `tests/test_api_server_security.py` 的 3,841 行断言一条不许改。
|
||
|
||
## 4. 硬红线
|
||
|
||
1. **`tests/test_api_server_security.py` 一条断言不许改。** 它红了说明不是纯搬运——回去改实现,不是改断言。这是判断「mixin 有没有搬坏」的主证据。
|
||
2. **方法体一字不改。** mixin 里的方法必须与搬走前逐字相同(除了必要的 import 调整)。允许用 `git diff -M` / `--color-moved` 之类的手段证明是移动而非改写,并把证明写进进度记录。
|
||
3. **mixin 模块不得 import `jyotish_api_server`。** 那等于把后门换了个地方开。要有断言钉住这个方向。
|
||
4. **不得改 `do_POST` / `do_GET` 的路由结构**(阶段 3 不做)。
|
||
5. 搬完后 `JyotishAPIHandler` 仍须通过继承拥有这些方法——**HTTP 侧的可见行为必须零变化**。
|
||
6. 不得顺手升级依赖、不得顺手修不在本单里的 warning。
|
||
|
||
## 5. 任务分解
|
||
|
||
### 5.1 重算闭包(已完成,结论见 §1)
|
||
|
||
执行方已按第一版 §5.1 重算并提交在 `dfd65528`(仅文档提交)。开工时以当时代码复核一次即可,不必从头再算。
|
||
|
||
### 5.2 spike 闸门(**必须先做,不产出正式交付**)
|
||
|
||
一次性验证,目的是回答「mixin 搬动会不会打红安全测试、会不会解不开循环 import」:
|
||
|
||
1. 把闭包里的方法整体移进 `ConsultationComputeMixin`,`JyotishAPIHandler(BaseHTTPRequestHandler, ConsultationComputeMixin)`。
|
||
2. **`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. 让步顺序
|
||
|
||
1. 5.2 spike **不得砍**——它是 B 与 A 的分水岭。
|
||
2. spike 绿则 5.3 + 5.4 必须一起做(只搬不改调用方,后门还在)。
|
||
3. spike 红则整单退化为 A,并在进度记录里写清楚,**不要硬做 B**。
|
||
4. 5.5、5.6 不得砍。
|
||
|
||
## 7. 开工前置命令
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
验收命令:
|
||
|
||
```bash
|
||
.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_POST` 78 个 elif 改路由表)——不立单,由门禁长期推进
|
||
- `execute_consultation_workflow` 里的任何业务逻辑改动(只能整体搬,不能改)
|
||
- `tests/` 下那 29 处 `__new__`(保持不增长即可)
|