# TASK · 关掉 `JyotishAPIHandler.__new__` 后门(314 行纯搬运) - 日期:2026-09-16 - 基线 commit:`origin/staging` @ `4f643aa0` - 执行分支:`codex/api-server-backdoor-close-20260916` - 落点:`scripts/jyotish_api_server.py`、新建的 `scripts/` 下模块、`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.2 - 与 `TASK-consultation-residual-hotspots-20260916`(`vedastro_*` / `yoga_engine`)、`TASK-home-state-lowering-batch2-20260916`(纯前端)**无文件重叠,三单可并行** - 规模:8 个方法 / 314 行搬进独立模块。**纯搬运,零行为变化。** --- ## 1. 为什么把范围缩到 314 行 原任务书四个阶段(拆 `__new__` → 抽 ≥150 行方法 → `do_POST` 改路由表 → 重新冻结)方向是对的,但它把「关后门」和「体量搬运」捆在了一起。我按调用链实测算了阶段 1 的**传递闭包**(从四处伪造实际借用的方法出发,跟着 `self._x()` 一路追): | | 数量 | | --- | ---: | | 需要搬的方法 | **8 个** | | 合计行数 | **314 行** | | 其中真的碰 HTTP 上下文(`self.headers` / `wfile` / `rfile` / `path` / `client_address`) | **0 个** | | 占 `JyotishAPIHandler` 全类(225 方法 / 8,219 行)的比例 | **4 % / 4 %** | **关后门只需要一次 314 行的纯搬运,而且这 8 个方法一个都不碰 HTTP,没有隐藏耦合。** 四处伪造实际借用的入口方法只有 5 个: | 位置 | 借用的方法 | | --- | --- | | `scripts/consultation_workflow_service.py:20` / `:27` | `_compute_vedastro_gateway_archives`、`_high_rigor_vedastro_official_summary`、`_interpretation_source_runtime_coverage` | | `scripts/capture_report_blocked_repairs_golden.py:59` | `_compute_consultation_workflow` | | `scripts/local_accuracy_report.py:141` | `_compute_synastry` | ## 2. 事故实证 ```python handler = JyotishAPIHandler.__new__(JyotishAPIHandler) # 不跑 __init__ 的空壳 ``` `__new__` 绕过 `BaseHTTPRequestHandler.__init__`,造出来的对象**没有 `headers` / `wfile` / `rfile` / `client_address`**。今天能跑,只是因为被借用的这 8 个方法碰巧没碰它们——我逐个查过,`0` 个碰。 **这是一颗类型检查看不见的地雷**:任何人往这 8 个方法(或它们调用的任何方法)里加一行 `self.headers.get(...)`,MCP 与两个离线脚本就会在运行时 `AttributeError`,而单元测试与 `tsc` 都发现不了。 更根本的是依赖方向反了:`consultation_workflow_service.py` 的 docstring 自称是「shared consultation workflow boundary for API and MCP callers」,实际却**反过来依赖 HTTP 单体文件**,只为借它身上的方法。 全仓依赖该模块的文件有 **43 个**(`scripts/`、`tests/`、`mcp_server.py`)。 ## 3. 决策记录 产品 2026-09-16 拍板: 1. **只做「关后门」这一件事,不做体量搬运。** 原任务书的阶段 2(抽 7 个 ≥150 行方法,2,073 行)与阶段 3(78 个 `elif` 改路由表)**本轮不做**。理由:那是 2,000+ 行的大搬运、零用户价值,而 `tests/test_api_server_security.py` 有 3,841 行断言绑在这个类上——风险与收益不匹配。 2. **阶段 2/3 不另立单,改由门禁长期推进。** `3b17c1b2` 已把增长冻结的主门从行数换成「**类方法数不得增长 + `__new__` 计数不得增长**」。这意味着以后任何想往这个类里加方法的改动都会被拦,只能往外搬——**换口径本身已经替代了一次性大搬运的必要性**。这条写进 `docs/tasks/README.md` 的状态板备注,避免下一轮有人又提。 3. **纯搬运,零行为变化。** `tests/test_api_server_security.py` 的 3,841 行断言**一条不许改**。 4. **收尾把 `__new__` 计数基线收到 0。** 这是本单唯一不可伪造的成功判据。 ## 4. 硬红线 1. **`grep -rn "JyotishAPIHandler.__new__" --include=*.py .`(排除 `skills/*/versions/**`)必须零命中**,并且这条断言要写进 `tests/test_api_server_growth_contract.py`,基线从 4 收到 **0**。 2. **`tests/test_api_server_security.py` 一条断言不许改。** 若它红了,说明不是纯搬运——回去改实现,不是改断言。 3. **不得顺手搬阶段 2/3 的方法。** 类方法数会因本单下降,把新值写进合同测试即可;**不得**为了多降一点而扩大范围。 4. 搬出去的模块**不得**再反向 import `jyotish_api_server`——那等于把后门换了个地方开。要有一条断言钉住这个方向。 5. 本单会与 `TASK-consultation-external-evidence-cache-20260915`(已合入 `e61535f4`)的改动共处同一文件:它在 `execute_consultation_workflow` 里加了走缓存模块的分支。**照常搬运,不得把它改回去。** 6. 不得顺手升级依赖、不得顺手修不在本单里的 warning。 ## 5. 任务分解 ### 5.1 先自己把闭包算一遍 不要抄本任务书的 8 个方法。开工时用同样的方法重算(从 5 个入口方法出发,跟着 `self._x()` 追传递闭包,并标出哪些碰 HTTP 上下文),结果写进进度记录。代码可能已经漂移。 - 验收:进度记录里有闭包清单、行数、以及「碰 HTTP 上下文的有几个」。 - 验收:若重算出来**有**方法碰 HTTP 上下文,**停手**,把那几个列出来先问——本单的前提是它们都不碰。 ### 5.2 把闭包搬进独立模块 按职责放进 `scripts/` 下的新模块(建议按 vedastro 证据 / 咨询工作流 / 合盘分组,不要一股脑塞一个文件)。`JyotishAPIHandler` 里对应位置改成薄调用。 - 验收:`scripts/jyotish_api_server.py` 的类方法数下降,新值写进合同测试。 - 验收:新模块不 import `jyotish_api_server`,有断言。 ### 5.3 三个调用方改成直接 import `consultation_workflow_service.py`(两处)、`capture_report_blocked_repairs_golden.py`、`local_accuracy_report.py` 改成直接 import 新模块,删掉 `JyotishAPIHandler.__new__` 与对 `jyotish_api_server` 的 import。 - 验收:三个文件都不再 import `jyotish_api_server`。 - 验收:MCP 侧(`mcp_server.py:741` / `:4749` 经由 `consultation_workflow_service`)仍然可用——至少一条端到端断言。 ### 5.4 合同测试收基线 `tests/test_api_server_growth_contract.py`:`__new__` 计数基线 4 → **0**;类方法数基线更新为收尾实测值;行数粗护栏 baseline 重设为收尾实测 + 300。同步更新文件顶部 docstring 的日期与说明。 - 验收:人为加回一处 `JyotishAPIHandler.__new__` 必须让测试变红(贴反向验证)。 - 验收:人为加一个类方法必须变红(贴反向验证)。 - 验收:该文件既有的三条断言(`AGENTS.md` 必须含 `must not grow` / `thinly registered`、该测试必须在 `CORE_PYTEST_TARGETS` 与快速门里)仍绿。 ### 5.5 记录 本单不产生 Bug 记录(结构改造,不是缺陷),不进 `CHANGELOG.md`(无用户可感知变化)。进度记录里必须有:闭包清单、搬前/搬后的类方法数与行数、两次反向验证结果。 同轮把原 `TASK-api-server-decomposition-20260916` 在状态板上标成**已取代(阶段 1 由本单完成;阶段 2/3 不立单,由门禁长期推进)**。 ## 6. 让步顺序 1. 5.1 **不得砍**——闭包没重算就动手,等于拿本任务书的旧数字赌代码没漂。 2. 5.2 + 5.3 是主体,必须一起做(只搬不改调用方,后门还在)。 3. 5.4 不得砍——`__new__` 计数收到 0 是本单唯一的成功判据。 4. 5.5 不得砍。 ## 7. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/api-server-backdoor-close-20260916 \ .worktrees/api-server-backdoor-close-20260916 origin/staging cd .worktrees/api-server-backdoor-close-20260916 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 ``` 开工前必读:`docs/research/pre_work_error_ledger.md`(AGENTS §9);原 `TASK-api-server-decomposition-20260916.md`(它对根因的分析仍然有效,只是范围被本单收窄)。 验收命令: ```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 .venv/bin/python scripts/run_quality_gate.py --profile quick ``` ## 8. BUG 编号起点 本单不占 BUG 号。基线 `4f643aa0` 上最大号 **BUG-733**;734/735 已被 `TASK-consultation-residual-hotspots-20260916` 预占。 ## 9. 不在本单范围 - 阶段 2(抽 ≥150 行业务方法)与阶段 3(`do_POST` 路由表)——**不立单,由门禁长期推进** - `execute_consultation_workflow` 里的任何业务逻辑改动 - 外网探测与 yoga 编译缓存(见 `TASK-consultation-residual-hotspots-20260916`)