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

12 KiB
Raw Blame History

PROGRESS · 关掉 JyotishAPIHandler.__new__ 后门

  • 日期:2026-09-16
  • 分支:codex/api-server-backdoor-close-20260916rebase 到 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 行
移动集还需要的模块级类 2BadRequestJobQueueFull
移动集还需要的模块级赋值 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.py412 行):

内容 成员
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_helpersVedastroEvidenceMixin()
  • scripts/local_accuracy_report.py:141SynastryMixin()._compute_synastry(...)

仍在用 __new__ 的 2 处(都挂在咨询工作流链上,属于 spike 红掉的那一半): scripts/consultation_workflow_service.py:20scripts/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 1129110924+300 粗护栏不变)
  • 类方法数 baseline 225218
  • __new__ 合计 baseline 3331
  • 新增 JYOTISH_API_HANDLER_NEW_SCRIPTS_BASELINE = 2test_scripts_handler_forgeries_must_not_grow, 单独锁 scripts 生产侧,注释写明目标仍是 0 及剩下 2 处为什么没关
  • 新增 test_offline_mixins_must_not_import_the_http_monolith(§4.3 的方向断言,ast 查 import
  • 原有三条断言(AGENTS.mdmust 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.py4 条)覆盖 §5.4 验收:

  1. SynastryMixin()._compute_synastryashtakoot.calculate_ashtakoot 逐字段一致(local_accuracy_report 路径)
  2. build_runtime_evidence_helpers 不伪造 handler 也能返回三个键(MCP 路径,端到端)
  3. ast 断言该函数体内已无 JyotishAPIHandler
  4. JyotishAPIHandler 仍持有全部 7 个方法、继承链正确(HTTP 侧零变化)

同时把该文件加进 scripts/run_quality_gate.pyCORE_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 45exit=0,六项全绿。

6.1 门禁外的定向抽查(BadRequest 搬家的下游)

BadRequestjyotish_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:20capture_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 测试)。
  • 阶段 3do_POST 路由表)按 §9 不做。
  • 本单不产生 Bug 记录,不进 CHANGELOG.md