Files
Jyotisha/docs/tasks/PROGRESS-api-server-backdoor-close-20260916.md
T
Jesse_ChenandClaude Opus 5 5582091851
Independent Staging Quality Gate / validate (push) Canceled after 2m18s
Independent Staging Quality Gate / publish (push) Canceled after 0s
refactor(api): 抽出 offline compute mixins,关掉 2/4 处 handler 伪造
spike 闸门(任务书 §5.2)结果为红,按 §6.3 退回 A 方案。

spike:115 个闭包方法整体移入 ConsultationComputeMixin 后,
tests/test_api_server_security.py 一字不改跑出 1 failed / 128 passed。
test_chart_async_job_executes_in_background 挂在
monkeypatch.setattr(jyotish_api_server, '_write_async_job_record', ...):
调用方法随 mixin 搬走后从新模块 globals 解析,补丁落在旧模块绑定上不生效。
已实证把同一 fake 打到 mixin 模块即恢复原行为,故为落点问题而非搬坏。
循环 import 不是障碍(移动集不引用 JyotishAPIHandler)。

退回 A 的实际交付:新建 scripts/offline_compute_mixins.py,
收 BadRequest、3 个模块级助手,以及 RequestParamMixin /
VedastroEvidenceMixin / SynastryMixin 共 7 个方法(300 行),
JyotishAPIHandler 通过继承保留全部方法,HTTP 侧零变化。
consultation_workflow_service.build_runtime_evidence_helpers 与
local_accuracy_report 改为直接实例化 mixin,不再伪造 handler。

11 个搬走的定义经 SHA-256 逐个比对与搬走前字节级相同;
jyotish_api_server.py 的 diff 为 11 行插入 / 378 行删除,无重排。

scripts 侧 __new__ 4 → 2(剩 2 处都在咨询工作流链上);
类方法 225 → 218;行数 11,291 → 10,924。
合同测试收紧基线并新增 scripts 侧专门断言与反向 import 断言;
两次反向验证(加回 __new__ / 加类方法)均正确变红。
新增 tests/test_offline_compute_mixins.py 覆盖此前零覆盖的 MCP 路径,
并加入 CORE_PYTEST_TARGETS。

tests/test_api_server_security.py 一字未改,129 passed。
快速门 pytest 段 798 passed / 1 skipped / 0 failed。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-16 03:46:01 +00:00

251 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS · 关掉 `JyotishAPIHandler.__new__` 后门
- 日期:2026-09-16
- 分支:`codex/api-server-backdoor-close-20260916`rebase 到 `origin/staging` @ `5094fd26`
- 任务书:`docs/tasks/TASK-api-server-backdoor-close-20260916.md`(第二版,B / mixin 方案)
- 结论:**spike 闸门红 → 按 §6.3 退回 A**。scripts 侧 `__new__` **4 → 2**,未归零。
---
## 1. §5.1 闭包复核(在 `dc8cae31` 代码上重算)
`ast` 解析,从四处伪造实际借用的入口出发,沿 `self._x()` **以及模块级函数的 `handler._x()`** 追传递闭包。
| | 实测 |
| --- | ---: |
| 闭包类方法 | **115** |
| 合计行数 | **4,104** |
| 碰 HTTP 上下文(`headers`/`wfile`/`rfile`/`path`/`client_address` | **0** |
| 闭包途经的模块级函数 | 65 个 / 2,028 行 |
| 移动集还需要的模块级类 | 2`BadRequest``JobQueueFull` |
| 移动集还需要的模块级赋值 | 26 |
与第二版任务书 §1 的数字一致,前提「0 个碰 HTTP」成立。
**循环 import 不是障碍**:移动集的自由名里**不含 `JyotishAPIHandler`**,方向是单向的 `jyotish_api_server → mixin`。任务书 §5.2 点名的这条风险,实测可解。
闭包按入口分裂:
| 入口 | 方法 | 行数 |
| --- | ---: | ---: |
| `_compute_vedastro_gateway_archives` | 1 | 4 |
| `_interpretation_source_runtime_coverage` | 1 | 41 |
| `_high_rigor_vedastro_official_summary` | 1 | 220 |
| `_compute_synastry` | 4 | 35 |
| `_compute_consultation_workflow` | **114** | **4,086** |
---
## 2. §5.2 spike 闸门:**红**
机械切分(源码行切片,方法体逐字搬运):115 方法 → `ConsultationComputeMixin`
`class JyotishAPIHandler(BaseHTTPRequestHandler, ConsultationComputeMixin)`
连同 65 个模块函数 / 2 个类 / 26 个赋值一并移入新模块。两文件均可解析、可 import,
MRO 正常(`_compute_synastry` / `_compute_consultation_workflow` 都能解析到)。
`tests/test_api_server_security.py` **一个字不改**直接跑:
```
1 failed, 128 passed in 43.17s
FAILED tests/test_api_server_security.py::test_chart_async_job_executes_in_background
```
### 2.1 红在哪、为什么
```python
monkeypatch.setattr(jyotish_api_server, '_write_async_job_record', fake_write) # 测试第 3768 行附近
...
assert writes[0][0] == 'api_chart_response' # IndexError: list index out of range
```
`_enqueue_chart_job` 随 mixin 搬到新模块后,它对 `_write_async_job_record` 的查找走
**新模块的 globals**;而测试把补丁打在 `jyotish_api_server` 的 globals 上。两个绑定不再是同一个,
补丁静默失效,`writes` 始终为空。
**这不是搬坏了代码**,已实证:把同一个 fake 改打到 mixin 模块上,行为与搬运前完全一致——
```
endpoint: chart_async mode: async_submitted status: queued
writes captured: 3 first scope: api_chart_response # 正是测试断言的值
```
### 2.2 为什么不能绕过
security 测试用 `jyotish_api_server.X` 打模块级补丁的名字共 6 个,其中 3 个落在移动集里
`_attach_vedastro_main_entry_overview``_load_local_module``_write_async_job_record`)。
任何 B 形态的切分,只要「被打补丁的函数」与「调用它的方法」一起离开 `jyotish_api_server` 的 globals
这条断言就会红。提到第三个共享模块也一样(`from third import X` 在两边各建一个绑定)。
唯一能保住绿的做法是给 `jyotish_api_server` 换一个自定义 module 类、劫持 `__setattr__` 往 mixin 模块转发——
那是新增机制、不是纯搬运,属于任务书 §6.3 明令的「硬做 B」。
按 §6.3**退回 A,不硬做 B。** spike 产物已丢弃,`scripts/` 回滚干净后重做。
---
## 3. 实际交付(A 方案)
新建 `scripts/offline_compute_mixins.py`412 行):
| 内容 | 成员 |
| --- | --- |
| `BadRequest` | 从 api server 搬出(api server 改为 import 回来,205 处引用不变) |
| 3 个模块函数 | `_build_vedastro_official_full_snapshot_payload_from_chart``_free_tier_queue_enabled_env``_preferred_strict_contract` |
| `RequestParamMixin` | `_get_float``_check_range``_normalize_degree` |
| `VedastroEvidenceMixin` | `_high_rigor_vedastro_official_summary``_compute_vedastro_gateway_archives``_interpretation_source_runtime_coverage` |
| `SynastryMixin(RequestParamMixin)` | `_compute_synastry` |
`class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryMixin)`
HTTP 侧通过继承拿回全部 7 个方法(`self._get_float` 等 74 处调用点一行未改)。
两个调用方改成直接实例化 mixin,不再伪造 handler
- `scripts/consultation_workflow_service.py:27` `build_runtime_evidence_helpers``VedastroEvidenceMixin()`
- `scripts/local_accuracy_report.py:141``SynastryMixin()._compute_synastry(...)`
**仍在用 `__new__` 的 2 处**(都挂在咨询工作流链上,属于 spike 红掉的那一半):
`scripts/consultation_workflow_service.py:20``scripts/capture_report_blocked_repairs_golden.py:59`
### 3.1 改前 / 改后
| 指标 | 改前 | 改后 |
| --- | ---: | ---: |
| `JyotishAPIHandler` 类方法数(`^ (?:async )?def` | 225 | **218** |
| `scripts/jyotish_api_server.py` 行数 | 11,291 | **10,924** |
| 新模块行数 | — | 412 |
| `__new__` scripts 侧 | 4 | **2** |
| `__new__` tests 侧 | 29 | 29(未动) |
| `__new__` 合计 | 33 | **31** |
### 3.2 §4.2「方法体一字不改」的证明
`git show HEAD:scripts/jyotish_api_server.py` 与新模块各自 `ast` 取定义源码切片,逐个 SHA-256 比对,
**11 个搬走的定义全部字节级相同**
```
IDENTICAL M:_get_float cf428670791b cf428670791b (10 lines)
IDENTICAL M:_check_range 5cb38a5550ba 5cb38a5550ba (5 lines)
IDENTICAL M:_normalize_degree b2ac5ec5533a b2ac5ec5533a (2 lines)
IDENTICAL M:_compute_synastry ad868c3b8e1f ad868c3b8e1f (18 lines)
IDENTICAL M:_high_rigor_vedastro_official_summary 42219ca0bef6 42219ca0bef6 (220 lines)
IDENTICAL M:_compute_vedastro_gateway_archives faa9643b7287 faa9643b7287 (4 lines)
IDENTICAL M:_interpretation_source_runtime_coverage 333b5c13515c 333b5c13515c (41 lines)
IDENTICAL T:_build_vedastro_official_full_snapshot_payload_from_chart d1cafe318865 d1cafe318865 (61 lines)
IDENTICAL T:_free_tier_queue_enabled_env 63a3fd275267 63a3fd275267 (7 lines)
IDENTICAL T:_preferred_strict_contract e251697dd292 e251697dd292 (6 lines)
IDENTICAL T:BadRequest 99667903a999 99667903a999 (2 lines)
```
搬运用按行区间删除实现,`jyotish_api_server.py` 的 diff 形态是
**11 行插入(import 块 + 类头一行)/ 378 行删除**,没有重排、没有空行规范化。
---
## 4. §5.5 合同测试
`tests/test_api_server_growth_contract.py`
- 行数 baseline `11291``10924`+300 粗护栏不变)
- 类方法数 baseline `225``218`
- `__new__` 合计 baseline `33``31`
- **新增** `JYOTISH_API_HANDLER_NEW_SCRIPTS_BASELINE = 2``test_scripts_handler_forgeries_must_not_grow`
单独锁 scripts 生产侧,注释写明目标仍是 0 及剩下 2 处为什么没关
- **新增** `test_offline_mixins_must_not_import_the_http_monolith`(§4.3 的方向断言,`ast` 查 import
- 原有三条断言(`AGENTS.md``must not grow` / `thinly registered`、该测试在 `CORE_PYTEST_TARGETS` 与快速门里)未改,仍绿
`7 passed`(原 5 条 + 新增 2 条)。
### 4.1 两次反向验证
**① 在 `scripts/` 加回一处 `JyotishAPIHandler.__new__`** → 必须红:
```
FAILED tests/test_api_server_growth_contract.py::test_jyotish_api_handler_new_count_must_not_grow
FAILED tests/test_api_server_growth_contract.py::test_scripts_handler_forgeries_must_not_grow
2 failed, 5 passed
```
**② 给 `JyotishAPIHandler` 加一个类方法**(218 → 219)→ 必须红:
```
FAILED tests/test_api_server_growth_contract.py::test_jyotish_api_handler_method_count_must_not_grow
1 failed, 6 passed
```
两次探针均已还原,工作树无残留。
---
## 5. 新增端到端守卫
`build_runtime_evidence_helpers` 原本**零测试覆盖**(只有 `mcp_server.py:4751` 在调)。
新增 `tests/test_offline_compute_mixins.py`4 条)覆盖 §5.4 验收:
1. `SynastryMixin()._compute_synastry``ashtakoot.calculate_ashtakoot` 逐字段一致(`local_accuracy_report` 路径)
2. `build_runtime_evidence_helpers` 不伪造 handler 也能返回三个键(MCP 路径,端到端)
3. `ast` 断言该函数体内已无 `JyotishAPIHandler`
4. `JyotishAPIHandler` 仍持有全部 7 个方法、继承链正确(HTTP 侧零变化)
同时把该文件加进 `scripts/run_quality_gate.py``CORE_PYTEST_TARGETS`——该列表自带注释
「不在这个清单里的守卫在 CI 里永远不跑」,本单的后门守卫必须进门禁。
---
## 6. §7 验收
```
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
→ 190 passed
```
`tests/test_api_server_security.py` 单独跑:**129 passed,文件一字未改**`git diff --stat tests/` 对该文件为空)。
快速门(退出码直接取,不隔管道):
```
.venv/bin/python scripts/run_quality_gate.py --profile quick → EXIT=127
pytest 段:798 passed, 1 skipped, 0 failed
失败步骤:step: npm test → sh: 1: tsx: not found
```
pytest 段 0 failed,且比 `dc8cae31` 基线(792 passed / 1 skipped**多 6 条**
正好是本轮新增的 4 条(`test_offline_compute_mixins.py`+ 2 条(合同测试)。
`tsx: not found` 是**本工作树没装 `frontend/node_modules`**(主检出 `/workspace/Jyotisha/frontend/node_modules` 有),
不是仓库缺口,也与本单纯 Python 改动无关,故未新开 BLK 条目。
开工预检 `python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45``exit=0`,六项全绿。
### 6.1 门禁外的定向抽查(`BadRequest` 搬家的下游)
`BadRequest``jyotish_api_server` 搬进新模块后由 api server import 回来,
仓内两个直接 `from jyotish_api_server import BadRequest` 的测试照常解析同一个类对象:
- `tests/test_declared_window_chart.py` —— 在快速门的 798 条里,通过
- `tests/test_active_rectification_api.py` —— **不在快速门**,定向跑:`1 failed / 50 passed`
那一条失败 `test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow`
**是基线即红,与本单无关**。在 `5094fd26` 的干净工作树上单独复跑同一条,
失败停在同一行(`tests/test_active_rectification_api.py:666`)、同一断言:
```
{'start_time': '04:16'} != {'start_time': '05:07'}
```
属校正打分的期望漂移,不涉及本轮搬走的任何名字,未修(§4.6 不得顺手修不在本单里的问题)。
---
## 7. 没做的 / 留给下一轮
- **scripts 侧 `__new__` 未归零**,剩 2 处:`consultation_workflow_service.py:20`
`capture_report_blocked_repairs_golden.py:59`。两者都要整条咨询工作流闭包(114 方法 / 4,086 行)搬走,
而搬走就会触发 §2.1 的 monkeypatch 落点问题。**下一轮要先解决的是那 3 个被模块级打补丁的名字怎么安置,不是体量。**
一个可行方向:先把 `_write_async_job_record` / `_load_local_module` /
`_attach_vedastro_main_entry_overview` 连同各自调用方一起留在 api server,
或把异步任务记录层单独抽成一个两边都 import 的模块并同步改测试落点(需产品批准动 security 测试)。
- 阶段 3`do_POST` 路由表)按 §9 不做。
- 本单不产生 Bug 记录,不进 `CHANGELOG.md`