# 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__`(保持不增长即可)