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
251 lines
12 KiB
Markdown
251 lines
12 KiB
Markdown
# 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`。
|