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
12 KiB
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 红在哪、为什么
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:27build_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__合计 baseline33→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 验收:
SynastryMixin()._compute_synastry与ashtakoot.calculate_ashtakoot逐字段一致(local_accuracy_report路径)build_runtime_evidence_helpers不伪造 handler 也能返回三个键(MCP 路径,端到端)ast断言该函数体内已无JyotishAPIHandlerJyotishAPIHandler仍持有全部 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。