@furongjun1999/dsh-memory 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +618 -572
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/README.md +1 -0
  5. package/docs/discipline/harnesses.yaml +31 -0
  6. package/docs/discipline/templates/full.md.tmpl +1 -1
  7. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  8. package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
  9. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
  10. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
  11. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
  12. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
  13. package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
  14. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
  15. package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
  16. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
  17. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
  18. package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
  19. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
  20. package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
  21. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
  22. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
  23. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
  24. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
  25. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
  26. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
  27. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
  28. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +52 -2
  29. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
  30. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -0
  31. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  32. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
  33. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
  34. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
  35. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
  36. package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
  37. package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
  38. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
  39. package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
  40. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
  41. package/lib/bridge.js +24 -2
  42. package/lib/hooks.d.ts +31 -2
  43. package/lib/hooks.js +219 -16
  44. package/lib/init.js +16 -1
  45. package/lib/lib/prompt_safety.d.ts +52 -1
  46. package/lib/lib/prompt_safety.js +76 -1
  47. package/md_cg/audit.py +652 -379
  48. package/md_cg/bench6_arms.py +1 -1
  49. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  50. package/md_cg/bench_p0.py +1 -1
  51. package/md_cg/branches.py +2 -2
  52. package/md_cg/ccgc.py +1071 -1006
  53. package/md_cg/chain.py +48 -1
  54. package/md_cg/consistency.py +97 -9
  55. package/md_cg/consolidate.py +34 -9
  56. package/md_cg/crypto.py +59 -26
  57. package/md_cg/docindex.py +16 -1
  58. package/md_cg/evolution.py +17 -1
  59. package/md_cg/export.py +1 -1
  60. package/md_cg/forgetting.py +309 -13
  61. package/md_cg/freshness.py +511 -0
  62. package/md_cg/fsutil.py +454 -8
  63. package/md_cg/generation.py +409 -0
  64. package/md_cg/hotcache.py +4 -1
  65. package/md_cg/identity.py +3 -3
  66. package/md_cg/insight.py +24 -3
  67. package/md_cg/linkref.py +1 -1
  68. package/md_cg/logref.py +327 -0
  69. package/md_cg/mcp_server.py +4261 -3818
  70. package/md_cg/mdcg.py +4695 -3547
  71. package/md_cg/mdcos.py +4672 -4159
  72. package/md_cg/mreview/pipeline.py +15 -1
  73. package/md_cg/nodefile.py +713 -576
  74. package/md_cg/protect.py +158 -11
  75. package/md_cg/protocol.py +41 -2
  76. package/md_cg/provenance.py +22 -4
  77. package/md_cg/reach.py +4 -4
  78. package/md_cg/readcache.py +76 -15
  79. package/md_cg/reconcile.py +1 -1
  80. package/md_cg/refindex.py +246 -37
  81. package/md_cg/refine.py +1 -1
  82. package/md_cg/routing.py +32 -5
  83. package/md_cg/run_tests.py +17 -0
  84. package/md_cg/scrub.py +30 -3
  85. package/md_cg/security.py +47 -1
  86. package/md_cg/self_state.py +5 -5
  87. package/md_cg/semantic/canonical.py +22 -0
  88. package/md_cg/semantic/unify.py +73 -22
  89. package/md_cg/semantic/unify_fixture.json +25 -0
  90. package/md_cg/sleep.py +1297 -0
  91. package/md_cg/sources.py +23 -6
  92. package/md_cg/srcindex.py +352 -0
  93. package/md_cg/stg.py +47 -3
  94. package/md_cg/subgraph.py +2 -2
  95. package/md_cg/sustain.py +1436 -1168
  96. package/md_cg/tasks.py +469 -470
  97. package/md_cg/test_auto_defaults.py +424 -0
  98. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  99. package/md_cg/test_b1b2_write_face.py +417 -0
  100. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  101. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  102. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  103. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  104. package/md_cg/test_boundary_hit.py +410 -0
  105. package/md_cg/test_c3_transient_read_negative.py +793 -0
  106. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  107. package/md_cg/test_ccg_form_parity.py +188 -0
  108. package/md_cg/test_ccgc.py +28 -3
  109. package/md_cg/test_en_pipeline.py +22 -13
  110. package/md_cg/test_generation_guard.py +352 -0
  111. package/md_cg/test_govern_directread.py +17 -4
  112. package/md_cg/test_h2_session_view_norm.py +233 -0
  113. package/md_cg/test_h4_sustain_snapshot.py +1387 -0
  114. package/md_cg/test_hive_ingest.py +285 -285
  115. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  116. package/md_cg/test_issue43_default_policy.py +865 -0
  117. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  118. package/md_cg/test_linkref.py +10 -3
  119. package/md_cg/test_lock.py +2 -2
  120. package/md_cg/test_logref.py +1109 -0
  121. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  122. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  123. package/md_cg/test_mr_m2.py +15 -3
  124. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  125. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  126. package/md_cg/test_n176_link_trust.py +221 -0
  127. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  128. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  129. package/md_cg/test_n195_writepath_reload.py +283 -0
  130. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  131. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  132. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  133. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  134. package/md_cg/test_n201_proposal_visibility.py +382 -0
  135. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  136. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  137. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  138. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  139. package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
  140. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  141. package/md_cg/test_n225_nonobject_load.py +1110 -0
  142. package/md_cg/test_n230_dirty_replay.py +375 -0
  143. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  144. package/md_cg/test_neg_condition_hits.py +333 -0
  145. package/md_cg/test_neg_tail_honesty.py +663 -0
  146. package/md_cg/test_none_id_write_guard.py +4 -3
  147. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  148. package/md_cg/test_p1.py +5 -5
  149. package/md_cg/test_p11_consistency.py +161 -12
  150. package/md_cg/test_p26_refindex.py +2 -2
  151. package/md_cg/test_p27_docindex.py +777 -774
  152. package/md_cg/test_p28_refcheck.py +740 -13
  153. package/md_cg/test_p29_session_ingest_export.py +2 -2
  154. package/md_cg/test_p2_mcp.py +12 -1
  155. package/md_cg/test_p2_six_elements.py +463 -0
  156. package/md_cg/test_p30_maintain.py +33 -2
  157. package/md_cg/test_p31_insight.py +1 -0
  158. package/md_cg/test_p3_legacy_closure.py +433 -0
  159. package/md_cg/test_p4_freshness.py +680 -0
  160. package/md_cg/test_p8_subgraph_chain.py +14 -1
  161. package/md_cg/test_p9c_dedup_hints.py +276 -0
  162. package/md_cg/test_policy_required_ccg.py +426 -0
  163. package/md_cg/test_protocol.py +22 -0
  164. package/md_cg/test_rank_parity_score_mode.py +522 -0
  165. package/md_cg/test_read_face_input_gates.py +363 -0
  166. package/md_cg/test_read_face_semantics.py +489 -0
  167. package/md_cg/test_recall_face_guards.py +812 -0
  168. package/md_cg/test_rejected_credential_forms.py +188 -0
  169. package/md_cg/test_rejected_redact.py +146 -0
  170. package/md_cg/test_retr_s1b.py +2 -2
  171. package/md_cg/test_retr_s5.py +20 -7
  172. package/md_cg/test_review_conformance.py +21 -3
  173. package/md_cg/test_security_audit_v21.py +2 -2
  174. package/md_cg/test_semantic_canonical.py +5 -3
  175. package/md_cg/test_server_version.py +67 -0
  176. package/md_cg/test_sleep.py +611 -0
  177. package/md_cg/test_sleep_p1.py +784 -0
  178. package/md_cg/test_srcindex.py +171 -0
  179. package/md_cg/test_tenant_env_override_warn.py +63 -50
  180. package/md_cg/test_time_core_lint.py +968 -0
  181. package/md_cg/test_token_lowercase_form.py +315 -0
  182. package/md_cg/test_unify_default_off.py +701 -0
  183. package/md_cg/test_unify_scope.py +182 -0
  184. package/md_cg/test_v21r1_package_version.py +310 -0
  185. package/md_cg/test_writepipe.py +10 -4
  186. package/md_cg/tokens.py +225 -95
  187. package/md_cg/tool_face.py +4 -4
  188. package/md_cg/trust.py +49 -5
  189. package/md_cg/units.py +204 -6
  190. package/md_cg/weights.py +4 -4
  191. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  192. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  193. package/md_cg/writelimit.py +42 -8
  194. package/md_cg/writepipe.py +651 -554
  195. package/package.json +4 -2
  196. package/skills/plugin.json +1 -1
  197. package/src/bridge.ts +25 -2
  198. package/src/hooks.ts +229 -16
  199. package/src/init.ts +15 -1
  200. package/src/lib/prompt_safety.ts +88 -1
  201. package/utf8_boot.py +237 -0
  202. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,403 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · stdio 主循环入口类型闸守卫(N206:一行非对象 JSON 杀整个 server)
3
+
4
+ 缺陷(修复前,本守卫红态实抓):``md_cg/mcp_server.py`` 的 stdin 主循环里
5
+ ``try`` 只包 ``json.loads`` 的 ValueError,随后**裸调** ``msg.get("method")``;
6
+ ``tools/call`` 分支又把 ``msg.get("params") or {}`` 的结果**裸调** ``.get("name")``。
7
+ 而 ``json.loads`` 的产物不必是对象——``[]`` / ``123`` / ``null`` / ``"x"`` / ``true``
8
+ (以及批量数组)都是**合法 JSON**。任何能写该 server stdin 的一方(MCP 宿主、
9
+ 被注入的宿主插件、stdio 管道交错写入者)发一行即得未捕获
10
+ ``AttributeError: 'list' object has no attribute 'get'`` 逃出 ``main()``,
11
+ 整个进程退出(rc=1,stdout 只到崩溃前那条)——工具面 tools/list 永无应答,
12
+ 崩溃期间记忆面(含写入)全不可用;零凭据可达。同族已在位:
13
+ ``hive/hive_mcp/mcp_server.py`` 的 ``_rpc`` 有 -32600/-32602 双闸 + main 入口兜底
14
+ (2026-09-25 v2-N16),md_cg 是**漏网面**(工具层 try 只包 ``call_tool``,
15
+ 包不到入口解析)。
16
+
17
+ 修复口径(与 hive 同族同修,fail-closed):入口把「解析结果类型」当判据——
18
+ 非 dict 行回 id=null 的 -32600 Invalid Request 并**继续服务**;
19
+ ``tools/call`` 的 params 非 dict(list/str/int)回 -32602 Invalid params 且不进工具;
20
+ ``_serve_line`` 整体兜底 -32603(单行请求的任何异常都不得杀 server)。
21
+
22
+ 守卫构成(①③④ 实弹子进程 = 红态在「进程死」,② 单测面,⑤ 源断言,⑥ floor):
23
+ ① 主场景一趟(若干攻击行夹击合法行):非对象行逐条得 -32600、非 dict params
24
+ 逐条得 -32602 且 id 回带、夹击后的 tools/list 与 initialize 全部应答、rc=0;
25
+ ② 单测面:``_serve_line`` 逐形态不抛异常 + 错误码正确 + 非对象/坏 params 形态
26
+ **先于一切 cg 触碰**(cg 传 @boom 探针——修前若真触 cg 会自曝);
27
+ ③ control 腿(只有 initialize+tools/list):rc=0、响应数 = 2(镜像缺陷材料);
28
+ ④ 攻击行在握手**之前**(首行即 `[]`):后续 initialize/tools/list 仍应答、rc=0;
29
+ ⑤ 源断言:主循环调用 `_serve_line` 且有入口兜底;`_serve_line` 内有
30
+ isinstance(msg, dict) / -32600 / -32602 / -32603 四判据;
31
+ ⑥ floor:实弹断言数不足即视为「spawn/扫描面失效」的假绿。
32
+
33
+ 隔离纪律(与 md_cg/test_issue39_utf8_stdio.py 同款):env 剔净全部 MDCG_*/HIVE_*
34
+ 注入面,只注入 `MDCG_ROOT`/`MDCG_AUX_ROOT`/`MDCG_STATE_ROOT`/`MDCG_DATA_ROOT`/
35
+ `MDCG_TENANT_REGISTRY`(指向不存在的临时路径=空表回落)/`MDCG_SUSTAIN=0`(关常驻
36
+ 循环)/`MDCG_LEGACY_ENV_AUTH=1`(既有豁免面,零令牌、不回落真实 ~/.mdcg);
37
+ 子进程 cwd=仓根,`-X utf8`;跑完 rmtree 临时根,并删掉**本子进程自己**的那份
38
+ 自报戳 `<tempdir>/md_cg_servers/<pid>.json`(诊断设施,非本次修复面)。
39
+ 绝不触真实令牌库、真实在役数据目录与真实 serve。
40
+
41
+ 运行:python -X utf8 -m md_cg.test_n206_stdio_jsonrpc_type
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import io
46
+ import json
47
+ import os
48
+ import shutil
49
+ import subprocess
50
+ import sys
51
+ import tempfile
52
+ from contextlib import redirect_stderr, redirect_stdout
53
+
54
+ try:
55
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
56
+ except AttributeError:
57
+ pass
58
+
59
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
60
+ LIVE_FLOOR = 12 # 实弹断言下限(防 spawn/扫描面失效假绿)
61
+
62
+ PASS, FAIL, LIVE = 0, 0, 0
63
+ FAILS = []
64
+
65
+
66
+ def check(name, cond, detail="", live=False):
67
+ global PASS, FAIL, LIVE
68
+ if live:
69
+ LIVE += 1
70
+ if cond:
71
+ PASS += 1
72
+ print(" [ok] %s" % name)
73
+ else:
74
+ FAIL += 1
75
+ FAILS.append(name)
76
+ print(" [FAIL] %s %s" % (name, detail))
77
+
78
+
79
+ # 非 dict JSON 攻击行(单行、合法 JSON、非 object)
80
+ NONDICT_LINES = [
81
+ ("batch_array", '[{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}]'),
82
+ ("empty_array", "[]"),
83
+ ("bare_int", "123"),
84
+ ("bare_null", "null"),
85
+ ("bare_str", '"x"'),
86
+ ("bare_true", "true"),
87
+ ]
88
+
89
+ # tools/call 的 params 非 dict 攻击(真值形态——假值会走 `or {}` 而侥幸不炸)
90
+ NONDICT_PARAMS = [
91
+ ("params_list", [1, 2]),
92
+ ("params_str", "x"),
93
+ ("params_int", 7),
94
+ ]
95
+
96
+ _HOSTILE_RIDS = (11, 12, 13) # 非 dict params 的请求 id(须原样回带)
97
+ _ABSENT_PARAMS_RID = 14 # params 缺省(原语义:落工具层错误,不得崩)
98
+
99
+
100
+ class _Boom:
101
+ """cg 探针:任何属性访问都自曝——证明入口闸先于一切 cg 触碰。"""
102
+
103
+ def __getattr__(self, item):
104
+ raise AssertionError("入口闸失效:非对象/坏 params 形态触碰了 cg.%s" % item)
105
+
106
+
107
+ # 模拟宿主前先把「身份/路径/编码」注入面清干净——子进程只吃临时目录
108
+ _DIRTY_KEYS = (
109
+ "MDCG_TOKEN", "MDCG_CLEARANCE", "MDCG_TENANT", "MDCG_TENANT_REGISTRY",
110
+ "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_LEGACY_ENV_AUTH",
111
+ "MDCG_LEGACY_ENV_ADMIN", "MDCG_SESSION", "DSH_SESSION_ID",
112
+ "MDCG_HARNESS", "MDCG_UNIT", "MDCG_ROOT", "MDCG_STATE_ROOT",
113
+ "MDCG_DATA_ROOT", "MDCG_AUX_ROOT", "MDCG_SUSTAIN", "MDCG_SUSTAIN_NAME",
114
+ "MDCG_MCP_SURFACE", "MDCG_TOOL_FACE", "MDCG_ACTOR",
115
+ "MDCG_VERIFIER_MODULES", "MDCG_HIVE_JOBS",
116
+ "PYTHONUTF8", "PYTHONIOENCODING", "PYTHONLEGACYWINDOWSSTDIO",
117
+ )
118
+
119
+
120
+ def _env(tmp: str) -> dict:
121
+ env = {k: v for k, v in os.environ.items() if k not in _DIRTY_KEYS}
122
+ env["MDCG_ROOT"] = os.path.join(tmp, "cgroot")
123
+ # 登记表指向**不存在**的临时路径 → 空表回落,绝不读真实 ~/.mdcg
124
+ env["MDCG_TENANT_REGISTRY"] = os.path.join(tmp, "_tenants_absent.json")
125
+ env["MDCG_STATE_ROOT"] = os.path.join(tmp, "state")
126
+ env["MDCG_DATA_ROOT"] = os.path.join(tmp, "data")
127
+ # 子目录名不可取 "aux"(Windows 保留设备名,见 test_issue39 头注)
128
+ env["MDCG_AUX_ROOT"] = os.path.join(tmp, "auxroot")
129
+ env["MDCG_SUSTAIN"] = "0" # 关常驻循环(线程面归零)
130
+ env["MDCG_LEGACY_ENV_AUTH"] = "1" # 既有豁免面:零令牌、无二次开关
131
+ env["MDCG_ACTOR"] = "n206-guard"
132
+ return env
133
+
134
+
135
+ def _talk(lines, tmp, timeout=180):
136
+ """spawn stdio server → 一次喂完 lines → (rc, stdout, stderr)。"""
137
+ proc = subprocess.Popen(
138
+ [sys.executable, "-X", "utf8", "-m", "md_cg.mcp_server"],
139
+ cwd=ROOT, env=_env(tmp), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
140
+ stderr=subprocess.PIPE, text=True, encoding="utf-8", errors="replace")
141
+ try:
142
+ out, err = proc.communicate("\n".join(lines) + "\n", timeout=timeout)
143
+ except subprocess.TimeoutExpired:
144
+ proc.kill()
145
+ out, err = proc.communicate()
146
+ # 自报戳残留(诊断设施,非本次修复面):只删本子进程自己的那份
147
+ try:
148
+ os.remove(os.path.join(tempfile.gettempdir(), "md_cg_servers",
149
+ "%d.json" % proc.pid))
150
+ except OSError:
151
+ pass
152
+ return proc.returncode, out or "", err or ""
153
+
154
+
155
+ def _resp_lines(out: str):
156
+ """stdout 逐行 → [(raw, obj_or_None)](解析失败的记 None,供「全行合法」断言)。"""
157
+ got = []
158
+ for ln in out.splitlines():
159
+ ln = ln.strip()
160
+ if not ln:
161
+ continue
162
+ try:
163
+ got.append((ln, json.loads(ln)))
164
+ except ValueError:
165
+ got.append((ln, None))
166
+ return got
167
+
168
+
169
+ def _err_code(obj):
170
+ return (obj.get("error") or {}).get("code") if isinstance(obj, dict) else None
171
+
172
+
173
+ def _rpc(rid, method, params=None):
174
+ msg = {"jsonrpc": "2.0", "id": rid, "method": method}
175
+ if params is not None:
176
+ msg["params"] = params
177
+ return json.dumps(msg, ensure_ascii=False)
178
+
179
+
180
+ def _src_window(src: str, start: str, end: str) -> str:
181
+ i = src.find(start)
182
+ if i < 0:
183
+ return ""
184
+ j = src.find(end, i)
185
+ return src[i:] if j < 0 else src[i:j]
186
+
187
+
188
+ def _mod():
189
+ try:
190
+ from . import mcp_server as M
191
+ return M
192
+ except Exception as exc: # noqa: BLE001 —— 诊断用
193
+ print(" [warn] 模块导入失败: %r" % (exc,))
194
+ return None
195
+
196
+
197
+ def _serve_unit(cg, msg):
198
+ """调 _serve_line(msg) → (返回值, 写出的行列表);函数缺失(修前)返回 ("<missing>", [])。"""
199
+ M = _mod()
200
+ if M is None or not hasattr(M, "_serve_line"):
201
+ return "<missing>", []
202
+ buf = io.StringIO()
203
+ with redirect_stdout(buf):
204
+ ret = M._serve_line(cg, msg)
205
+ return ret, [ln for ln in buf.getvalue().splitlines() if ln.strip()]
206
+
207
+
208
+ def main():
209
+ tmp = tempfile.mkdtemp(prefix="mdcg_n206_")
210
+ try:
211
+ # ---------- ① 主场景:攻击行夹击合法行(实弹) ----------
212
+ print("[1] 实弹主场景:6 非对象行 + 3 非 dict params + 缺省 params,"
213
+ "夹击 initialize/tools/list")
214
+ lines = [_rpc(1, "initialize")]
215
+ lines += [raw for _tag, raw in NONDICT_LINES]
216
+ for (rid, (_tag, bad)) in zip(_HOSTILE_RIDS, NONDICT_PARAMS):
217
+ lines.append(json.dumps({"jsonrpc": "2.0", "id": rid,
218
+ "method": "tools/call", "params": bad}))
219
+ lines.append(_rpc(_ABSENT_PARAMS_RID, "tools/call"))
220
+ lines.append(_rpc(2, "tools/list"))
221
+ lines.append(_rpc(3, "tools/list"))
222
+ lines.append(_rpc(4, "shutdown"))
223
+ rc, out, err = _talk(lines, tmp)
224
+ resps = _resp_lines(out)
225
+ objs = [o for _raw, o in resps]
226
+ by_id = {o.get("id"): o for o in objs if isinstance(o, dict)}
227
+
228
+ check("1a 进程存活到正常下线 rc=0(红态:攻击行后 rc=1 崩死)",
229
+ rc == 0, f"rc={rc} stdout_lines={len(resps)} stderr_tail={err[-300:]!r}",
230
+ live=True)
231
+ check("1b stdout 每一行都是合法 JSON(无半截/串行响应)",
232
+ all(o is not None for _raw, o in resps),
233
+ f"bad_lines={[r for r, o in resps if o is None][:3]}", live=True)
234
+ nul_32600 = [o for o in objs if isinstance(o, dict) and o.get("id") is None
235
+ and _err_code(o) == -32600]
236
+ check("1c 每条非对象行各得一条 id=null 的 -32600 Invalid Request",
237
+ len(nul_32600) == len(NONDICT_LINES),
238
+ f"got={len(nul_32600)} want={len(NONDICT_LINES)} "
239
+ f"objs={json.dumps(objs, ensure_ascii=False)[:300]}", live=True)
240
+ got_32602 = [r for r in _HOSTILE_RIDS if _err_code(by_id.get(r)) == -32602]
241
+ check("1d 非 dict params 的 tools/call 逐条回 -32602 且 id 原样回带",
242
+ got_32602 == list(_HOSTILE_RIDS),
243
+ f"got={got_32602} want={list(_HOSTILE_RIDS)} "
244
+ f"resp={str(by_id.get(_HOSTILE_RIDS[0]))[:200]}", live=True)
245
+ init = by_id.get(1) or {}
246
+ check("1e 攻击行夹击下 initialize 仍应答(serverInfo=mdcg-mcp)",
247
+ (init.get("result") or {}).get("serverInfo", {}).get("name")
248
+ == "mdcg-mcp", f"got={str(init)[:200]}", live=True)
249
+ for rid in (2, 3):
250
+ t = (by_id.get(rid) or {}).get("result") or {}
251
+ check(f"1f tools/list(id={rid}) 在攻击行之后仍应答且工具表非空",
252
+ isinstance(t.get("tools"), list) and len(t["tools"]) > 0,
253
+ f"got={str(by_id.get(rid))[:200]}", live=True)
254
+ check("1g params 缺省的 tools/call 有应答(原语义落工具层错误,不得静默/崩)",
255
+ isinstance(by_id.get(_ABSENT_PARAMS_RID), dict),
256
+ f"got={str(out)[-200:]!r}", live=True)
257
+ check("1h shutdown 有应答(进程不是被攻击行提前带走)",
258
+ isinstance(by_id.get(4), dict), f"got={str(out)[-200:]!r}", live=True)
259
+ check("1i stderr 无 AttributeError / Traceback(旧态逃逸印迹)",
260
+ "AttributeError" not in err
261
+ and "Traceback (most recent call last)" not in err,
262
+ f"err_tail={err[-300:]!r}", live=True)
263
+ check("1j 隔离自证:记忆库落在守卫自己的临时根(cgroot 已建)",
264
+ os.path.isdir(os.path.join(tmp, "cgroot")),
265
+ f"tmp={os.listdir(tmp)}", live=True)
266
+
267
+ # ---------- ② 单测面:入口闸先于一切 cg 触碰 ----------
268
+ print("[2] 单测面 _serve_line(cg 传 @boom 探针)")
269
+ for tag, raw in NONDICT_LINES:
270
+ try:
271
+ ret, wy = _serve_unit(_Boom(), json.loads(raw))
272
+ except Exception as exc: # noqa: BLE001 —— 红态正是抛异常
273
+ check(f"2·{tag} 非对象行不抛异常", False,
274
+ f"抛 {type(exc).__name__}: {exc}")
275
+ continue
276
+ parsed = []
277
+ for ln in wy:
278
+ try:
279
+ parsed.append(json.loads(ln))
280
+ except ValueError:
281
+ parsed.append(None)
282
+ check(f"2·{tag} 非对象行回 id=null 的 -32600 且返回 False(未触碰 cg)",
283
+ ret is False and len(parsed) == 1 and isinstance(parsed[0], dict)
284
+ and parsed[0].get("id") is None and _err_code(parsed[0]) == -32600,
285
+ f"ret={ret!r} lines={wy}")
286
+ for (tag, bad) in NONDICT_PARAMS:
287
+ msg = {"jsonrpc": "2.0", "id": 901, "method": "tools/call",
288
+ "params": bad}
289
+ try:
290
+ ret, wy = _serve_unit(_Boom(), msg)
291
+ except Exception as exc: # noqa: BLE001
292
+ check(f"2·{tag} 不抛异常", False, f"抛 {type(exc).__name__}: {exc}")
293
+ continue
294
+ p = json.loads(wy[0]) if wy else None
295
+ check(f"2·{tag} 回 -32602 且 id 回带(未触碰 cg)",
296
+ ret is False and isinstance(p, dict) and p.get("id") == 901
297
+ and _err_code(p) == -32602, f"ret={ret!r} lines={wy}")
298
+ # 合法面零回归(同样以 _Boom 作 cg——这些分支本就不该碰 cg)
299
+ ret, wy = _serve_unit(_Boom(), {"jsonrpc": "2.0",
300
+ "method": "notifications/initialized"})
301
+ check("2·notification 仍无响应(返回 False、零写出)",
302
+ ret is False and wy == [], f"ret={ret!r} lines={wy}")
303
+ ret, wy = _serve_unit(_Boom(), {"jsonrpc": "2.0", "id": 9,
304
+ "method": "no/such"})
305
+ p = json.loads(wy[0]) if wy else None
306
+ check("2·未知方法仍 -32601 且 id 回带",
307
+ ret is False and isinstance(p, dict) and p.get("id") == 9
308
+ and _err_code(p) == -32601, f"ret={ret!r} lines={wy}")
309
+ ret, wy = _serve_unit(_Boom(), {"jsonrpc": "2.0", "id": 10,
310
+ "method": "shutdown"})
311
+ p = json.loads(wy[0]) if wy else None
312
+ check("2·shutdown 回空结果并返回 True(调用方据此跳出读循环)",
313
+ ret is True and isinstance(p, dict) and p.get("id") == 10
314
+ and p.get("result") == {}, f"ret={ret!r} lines={wy}")
315
+ # 入口兜底(-32603):分派体内抛异常(未来演化面)也只回一行错误、不抛出,
316
+ # 且必须开口(stderr 留痕,「不静默」)。
317
+ M = _mod()
318
+ if M is None or not hasattr(M, "_serve_line"):
319
+ check("2·分派体异常回 -32603 且不抛出(入口兜底)", False,
320
+ "缺 _serve_line(修前形态)")
321
+ else:
322
+ _orig = M.tools_for_surface
323
+
324
+ def _boom_tools():
325
+ raise RuntimeError("simulated-tools-surface-fault")
326
+
327
+ M.tools_for_surface = _boom_tools
328
+ ebuf = io.StringIO()
329
+ try:
330
+ with redirect_stderr(ebuf):
331
+ ret, wy = _serve_unit(_Boom(), {"jsonrpc": "2.0", "id": 5,
332
+ "method": "tools/list"})
333
+ finally:
334
+ M.tools_for_surface = _orig
335
+ p = json.loads(wy[0]) if wy else None
336
+ check("2·分派体异常回 -32603 且不抛出(入口兜底不崩 server)",
337
+ ret is False and isinstance(p, dict) and p.get("id") == 5
338
+ and _err_code(p) == -32603,
339
+ f"ret={ret!r} lines={wy}")
340
+ check("2·入口兜底必须开口(stderr 留痕,不静默)",
341
+ "主循环兜底" in ebuf.getvalue(), f"stderr={ebuf.getvalue()!r}")
342
+
343
+ # ---------- ③ control 腿(镜像缺陷材料的对照跑法) ----------
344
+ print("[3] control 腿:只有 initialize + tools/list")
345
+ rc_c, out_c, err_c = _talk([_rpc(1, "initialize"), _rpc(2, "tools/list")],
346
+ tmp)
347
+ cs = _resp_lines(out_c)
348
+ check("3a control 腿 rc=0 且恰 2 条应答(无新增杂散响应)",
349
+ rc_c == 0 and len(cs) == 2,
350
+ f"rc={rc_c} n={len(cs)} out={out_c[-200:]!r} err={err_c[-200:]!r}",
351
+ live=True)
352
+ check("3b control 腿 stderr 无 Traceback",
353
+ "Traceback (most recent call last)" not in err_c,
354
+ f"err_tail={err_c[-300:]!r}", live=True)
355
+
356
+ # ---------- ④ 攻击行在握手之前(首行即 []) ----------
357
+ print("[4] 首行即攻击行(握手前):后续 initialize/tools/list 仍须应答")
358
+ rc_d, out_d, err_d = _talk(["[]", _rpc(1, "initialize"),
359
+ _rpc(2, "tools/list")], tmp)
360
+ ds = _resp_lines(out_d)
361
+ d_ids = {o.get("id") for _r, o in ds if isinstance(o, dict)}
362
+ check("4a 首行 [] 后 rc=0 且 initialize(id=1)+tools/list(id=2) 均应答",
363
+ rc_d == 0 and {1, 2} <= d_ids,
364
+ f"rc={rc_d} ids={sorted(x for x in d_ids if x is not None)} "
365
+ f"err_tail={err_d[-300:]!r}", live=True)
366
+ check("4b 首行 [] 得 id=null 的 -32600",
367
+ any(isinstance(o, dict) and o.get("id") is None
368
+ and _err_code(o) == -32600 for _r, o in ds),
369
+ f"out={out_d[:200]!r}", live=True)
370
+
371
+ # ---------- ⑤ 源断言(锚点:闸在入口里,且主循环调用它) ----------
372
+ print("[5] 源断言")
373
+ with open(os.path.join(ROOT, "md_cg", "mcp_server.py"),
374
+ encoding="utf-8", errors="replace") as fh:
375
+ src = fh.read()
376
+ entry = _src_window(src, "def _serve_line", "def main()")
377
+ check("5a _serve_line 入口含 isinstance(msg, dict) 类型闸",
378
+ "isinstance(msg, dict)" in entry, f"window_len={len(entry)}")
379
+ check("5b 入口四错误码齐备(-32600 Invalid Request / -32602 Invalid "
380
+ "params / -32601 未知方法 / -32603 兜底)",
381
+ all(c in entry for c in ("-32600", "-32602", "-32601", "-32603")),
382
+ f"window_len={len(entry)}")
383
+ loop = _src_window(src, "for line in sys.stdin", "sustain.stop_all()")
384
+ check("5c 主循环体把每行交给 _serve_line 且带入口兜底 except",
385
+ "_serve_line(cg, msg)" in loop and "except Exception" in loop
386
+ and "-32603" in loop, f"window_len={len(loop)}")
387
+
388
+ # ---------- ⑥ floor 自检 ----------
389
+ print("[6] 守卫自检")
390
+ check(f"6a 实弹断言数 ≥ {LIVE_FLOOR}(防 spawn/扫描面失效假绿)",
391
+ LIVE >= LIVE_FLOOR, f"LIVE={LIVE}")
392
+ finally:
393
+ shutil.rmtree(tmp, ignore_errors=True)
394
+
395
+ print("\n=== N206 stdio jsonrpc type gate: %d passed, %d failed (live=%d) ==="
396
+ % (PASS, FAIL, LIVE))
397
+ if FAILS:
398
+ print("失败项:%s" % ";".join(FAILS))
399
+ return 1 if FAIL else 0
400
+
401
+
402
+ if __name__ == "__main__":
403
+ sys.exit(main())