@furongjun1999/dsh-memory 0.5.0 → 0.5.1

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 (138) hide show
  1. package/README.md +552 -465
  2. package/docs/README.md +1 -0
  3. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  4. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  5. package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
  6. package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
  7. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
  8. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
  9. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
  10. 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_v1.md +156 -0
  11. 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_v10.md +210 -0
  12. 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_v11.md +227 -0
  13. 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_v12.md +203 -0
  14. 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_v13.md +233 -0
  15. 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_v14.md +191 -0
  16. 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_v15.md +213 -0
  17. 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_v16.md +214 -0
  18. 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_v17.md +199 -0
  19. 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_v2.md +156 -0
  20. 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_v3.md +152 -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_v4.md +128 -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_v5.md +114 -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_v6.md +192 -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_v7.md +187 -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_v8.md +207 -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_v9.md +203 -0
  27. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
  28. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +19 -3
  29. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  30. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -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/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
  33. package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
  34. package/lib/bridge.d.ts +9 -0
  35. package/lib/bridge.js +35 -0
  36. package/lib/index.js +7 -1
  37. package/lib/lib/roleplay_web.js +530 -443
  38. package/lib/lib/token_store.d.ts +7 -1
  39. package/lib/lib/token_store.js +12 -3
  40. package/md_cg/audit.py +12 -1
  41. package/md_cg/backfill.py +16 -15
  42. package/md_cg/backfill_bucket_zh.py +35 -0
  43. package/md_cg/bench_e2e_judge.py +532 -0
  44. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  45. package/md_cg/bench_e2e_qa.py +256 -0
  46. package/md_cg/branches.py +18 -2
  47. package/md_cg/ccgc.py +3 -2
  48. package/md_cg/chain.py +19 -4
  49. package/md_cg/consolidate.py +7 -6
  50. package/md_cg/crosscheck.py +4 -3
  51. package/md_cg/crypto.py +439 -437
  52. package/md_cg/datapath.py +395 -335
  53. package/md_cg/evidence.py +582 -580
  54. package/md_cg/export.py +3 -1
  55. package/md_cg/forgetting.py +2 -2
  56. package/md_cg/fsutil.py +48 -0
  57. package/md_cg/hotcache.py +255 -238
  58. package/md_cg/interop.py +161 -22
  59. package/md_cg/judgment_manifest.py +177 -0
  60. package/md_cg/links.py +655 -622
  61. package/md_cg/mcp_server.py +280 -29
  62. package/md_cg/mdcg.py +616 -125
  63. package/md_cg/mdcos.py +430 -66
  64. package/md_cg/mreview/govern.py +5 -4
  65. package/md_cg/postings.py +4 -2
  66. package/md_cg/readcache.py +76 -18
  67. package/md_cg/reconcile.py +228 -0
  68. package/md_cg/review_cli.py +170 -0
  69. package/md_cg/routing.py +28 -0
  70. package/md_cg/run_tests.py +211 -0
  71. package/md_cg/scrub.py +862 -852
  72. package/md_cg/security.py +385 -275
  73. package/md_cg/selfreport.py +3 -2
  74. package/md_cg/signer.py +565 -562
  75. package/md_cg/sources.py +3 -2
  76. package/md_cg/stg.py +6 -0
  77. package/md_cg/sustain.py +1168 -1138
  78. package/md_cg/test_access_hints.py +147 -0
  79. package/md_cg/test_branch_discard_tombstone.py +136 -0
  80. package/md_cg/test_branches.py +259 -249
  81. package/md_cg/test_chain_read_isolate.py +168 -0
  82. package/md_cg/test_datapath_device_name.py +203 -0
  83. package/md_cg/test_emit_negtail_cache.py +156 -0
  84. package/md_cg/test_en_pipeline.py +186 -166
  85. package/md_cg/test_govern_directread.py +421 -0
  86. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  87. package/md_cg/test_identity_attribution.py +228 -147
  88. package/md_cg/test_index_durability.py +238 -224
  89. package/md_cg/test_interop.py +4 -2
  90. package/md_cg/test_interop_judgment.py +228 -0
  91. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  92. package/md_cg/test_links_concurrent_write.py +188 -0
  93. package/md_cg/test_merge_upsert.py +168 -0
  94. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  95. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  96. package/md_cg/test_n131_merge_gate.py +205 -0
  97. package/md_cg/test_p1x_ref_root.py +160 -0
  98. package/md_cg/test_p27_docindex.py +774 -765
  99. package/md_cg/test_p2_mcp.py +3 -0
  100. package/md_cg/test_p32_backfill.py +304 -298
  101. package/md_cg/test_p39_verify_flow.py +90 -50
  102. package/md_cg/test_p47_session_view.py +60 -25
  103. package/md_cg/test_propose_tail_index.py +157 -0
  104. package/md_cg/test_read_scope_b27.py +277 -0
  105. package/md_cg/test_readcache_default_on.py +168 -0
  106. package/md_cg/test_readcache_precise_inval.py +270 -0
  107. package/md_cg/test_readcache_prodpath.py +55 -7
  108. package/md_cg/test_reconcile_v0.py +294 -0
  109. package/md_cg/test_retr_s1.py +6 -2
  110. package/md_cg/test_retr_s1b.py +67 -0
  111. package/md_cg/test_retr_s7.py +8 -0
  112. package/md_cg/test_retr_s9_entity_ctx.py +181 -175
  113. package/md_cg/test_retr_score_once.py +208 -0
  114. package/md_cg/test_review_onepass.py +170 -0
  115. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  116. package/md_cg/test_security_audit.py +155 -0
  117. package/md_cg/test_security_audit_b26.py +161 -0
  118. package/md_cg/test_security_audit_v21.py +250 -0
  119. package/md_cg/test_semantic_canonical.py +255 -241
  120. package/md_cg/test_session_isolation.py +168 -0
  121. package/md_cg/test_snapshot_autoclose.py +187 -0
  122. package/md_cg/test_tail_watermark_race.py +208 -0
  123. package/md_cg/test_tenant_env_override_warn.py +139 -0
  124. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  125. package/md_cg/test_v14_fixes.py +415 -397
  126. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  127. package/md_cg/theory.py +276 -273
  128. package/md_cg/tokens.py +734 -677
  129. package/md_cg/units.py +3 -2
  130. package/md_cg/vision_evidence.py +4 -3
  131. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  132. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  133. package/md_cg/writepipe.py +554 -550
  134. package/package.json +6 -2
  135. package/src/bridge.ts +434 -401
  136. package/src/index.ts +526 -518
  137. package/src/lib/roleplay_web.ts +1019 -932
  138. package/src/lib/token_store.ts +202 -192
@@ -0,0 +1,185 @@
1
+ # -*- coding: utf-8 -*-
2
+ """N130 守卫:verify(verdict="falsified") 的删除必须过「不可遗忘」保护。
3
+
4
+ 缺陷形态(2026-09-25 引擎域新发现,编号 N130,high):verify() 的 falsified
5
+ 分支(mdcg.py:3188-3195)对原节点 os.remove **永久硬删**,全程不经
6
+ protect.guard_forget / require_layer_write / override——self/anchor 层、
7
+ protected 标记、importance≥0.7 的受保护节点可被一次 verify 调用一键删除,
8
+ 无快照、无留痕、不进 trash;对照 forget 路径(mdcos.py:2294)同一个删除
9
+ 动作需 require_admin + guard_forget 双闸(旧版本先快照、动作全程留痕)。
10
+
11
+ 攻击面:任何持 verify op 的主体(verify 角色 ops_allow 即含 verify,
12
+ designer ops=["*"] 天然含)对任意 node_id 调 cg(op=verify,
13
+ verdict=falsified) 一次调用永久删除;无需 override 无需 admin。
14
+
15
+ 修复:①falsified 分支在任何副作用(add_rejected / os.remove)之前接
16
+ protect.guard_forget(受保护节点需显式 override=True——旧版本自动快照 +
17
+ _protected_audit 留痕,与 forget 路径同一语义;拒绝抛 ProtectionError,
18
+ 原节点原样保留);②MdCGSecure.verify 对 falsified 按原节点层做
19
+ require_layer_write(与 add/add_rejected 同一闸口——verify 角色
20
+ layers_allow=["rejected","contextual"],本就「不得改被验证内容」,
21
+ tokens.py:171 显式 forbidden)。
22
+
23
+ 红项 = R1-R7(四类受保护节点一键硬删复现 + secure 面层写缺口);
24
+ G 项锁语义不变(普通节点 falsified 负记忆化、override 显式放行 + 快照留痕)。
25
+ 运行:python -m md_cg.test_n130_verify_falsified_protect
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ import shutil
31
+ import sys
32
+ import tempfile
33
+
34
+ try:
35
+ from . import protect
36
+ from .mdcos import MdCGOS, MdCGSecure
37
+ from .security import AccessDenied, Principal
38
+ except ImportError: # 直接脚本运行
39
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
40
+ from md_cg import protect
41
+ from md_cg.mdcos import MdCGOS, MdCGSecure
42
+ from md_cg.security import AccessDenied, Principal
43
+
44
+ PASS = FAIL = 0
45
+ FAILS = []
46
+
47
+
48
+ def ok(cond, label):
49
+ global PASS, FAIL
50
+ if cond:
51
+ PASS += 1
52
+ print(f" OK {label}")
53
+ else:
54
+ FAIL += 1
55
+ FAILS.append(label)
56
+ print(f" FAIL {label}")
57
+
58
+
59
+ _BODY = "# 功能名:N130 守卫\n\n# 生效条件:无条件\n\n正文。\n"
60
+
61
+
62
+ def _exists(cg, nid):
63
+ """索引条目在且盘面文件在(硬删后两者都会消失)。"""
64
+ e = (cg.index.get("nodes") or {}).get(nid)
65
+ return (e is not None
66
+ and os.path.exists(os.path.join(cg.root, e["path"])))
67
+
68
+
69
+ def _falsified_must_reject(cg, nid, label):
70
+ """对 nid 做 falsified:期望 ProtectionError 且节点原样保留。"""
71
+ raised = None
72
+ try:
73
+ cg.verify(nid, "N130 守卫的反例证据", "falsified")
74
+ except protect.ProtectionError as exc:
75
+ raised = exc
76
+ ok(raised is not None, f"{label}1 falsified 受保护节点被拒(ProtectionError)")
77
+ ok(_exists(cg, nid), f"{label}2 拒绝后索引与盘面文件原样保留")
78
+
79
+
80
+ def _seed(cg):
81
+ """四类受保护节点 + 一类普通节点(对照)。"""
82
+ cg.add("n_self", _BODY, layer="self")
83
+ cg.add("n_anchor", _BODY, layer="anchor")
84
+ cg.add("n_prot", _BODY, layer="knowledge")
85
+ protect.mark(cg, "n_prot", "N130 守卫:显式保护标记")
86
+ cg.add("n_imp", _BODY, layer="knowledge", importance=0.9)
87
+ cg.add("n_plain", _BODY, layer="knowledge")
88
+
89
+
90
+ def _base_shape(tmp):
91
+ """A:基类引擎面(无令牌)——四类受保护节点不得被 falsified 一键硬删。"""
92
+ print("--- A 基类引擎面:guard_forget 前置")
93
+ root = os.path.join(tmp, "base")
94
+ cg = MdCGOS(root)
95
+ _seed(cg)
96
+
97
+ _falsified_must_reject(cg, "n_self", "R1_self")
98
+ _falsified_must_reject(cg, "n_anchor", "R2_anchor")
99
+ _falsified_must_reject(cg, "n_prot", "R3_protected")
100
+ _falsified_must_reject(cg, "n_imp", "R4_importance0.9")
101
+
102
+ ok(all(_exists(cg, n) for n in
103
+ ("n_self", "n_anchor", "n_prot", "n_imp")),
104
+ "R5 四次拒绝后四个受保护节点全部健在")
105
+ ok(not os.path.isdir(os.path.join(cg.root, protect.HISTORY_DIR)),
106
+ "R6 拒绝路径零副作用(未创建 _protected_history)")
107
+
108
+ # G1:普通节点 falsified 语义不变(负记忆化 + 原节点移除)
109
+ r = cg.verify("n_plain", "普通节点反例", "falsified")
110
+ ok(r.get("action") == "falsified", "G1a 普通 knowledge 节点 falsified 照常放行")
111
+ ok(not _exists(cg, "n_plain"), "G1b 原 knowledge 节点按负记忆化移除(语义不变)")
112
+ neg = [e for e in (cg.index.get("nodes") or {}).values()
113
+ if e.get("layer") == "rejected"]
114
+ ok(len(neg) == 1, "G1c rejected 层出现负记忆条目(语义不变)")
115
+
116
+ # G2:override=True 显式放行 + 快照留痕(与 forget 路径同一能力面)
117
+ r2 = cg.verify("n_prot", "显式 override 的反例", "falsified", override=True)
118
+ ok(r2.get("action") == "falsified", "G2a override=True 显式放行 falsified")
119
+ ok(not _exists(cg, "n_prot"), "G2b override 放行后原节点移除")
120
+ hist = protect.history(cg, "n_prot")
121
+ ok(len(hist) == 1, "G2c override 放行前旧版本已快照进 _protected_history")
122
+ audit = os.path.join(cg.root, protect.AUDIT_FILE)
123
+ ok(os.path.exists(audit) and "override_forget" in open(
124
+ audit, encoding="utf-8").read(),
125
+ "G2d override_forget 动作已留痕 _protected_audit.jsonl")
126
+ cg.close()
127
+
128
+
129
+ def _secure_shape(tmp):
130
+ """B:MdCGSecure 面——falsified 删除按原节点层做 require_layer_write。"""
131
+ print("--- B 安全面:层写权限校验")
132
+ root = os.path.join(tmp, "secure")
133
+ verify_p = Principal(actor="verifier-1", role="verify", can_write=True,
134
+ can_admin=False, clearance="internal",
135
+ layers_allow=["rejected", "contextual"],
136
+ ops_allow=["info", "read", "verify"])
137
+ designer_p = Principal(actor="designer-1", role="designer", can_write=True,
138
+ can_admin=True, clearance="secret")
139
+ # 预置用 designer(knowledge 层只有可写该层的身份能建),攻击用 verify 角色
140
+ cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
141
+ cg.add("s_kn", _BODY, layer="knowledge") # verify 角色无权写 knowledge
142
+ cg.add("s_ctx", _BODY, layer="contextual") # verify 角色本职可写
143
+ cg.close()
144
+
145
+ # R7:verify 角色 falsified knowledge 节点 → 层写拒绝,节点原样保留
146
+ cg = MdCGSecure(root, principal=verify_p, master_key=bytes(range(32)))
147
+ raised = None
148
+ try:
149
+ cg.verify("s_kn", "verify 角色反例", "falsified")
150
+ except AccessDenied as exc:
151
+ raised = exc
152
+ ok(raised is not None, "R7a verify 角色 falsified knowledge 层被拒(AccessDenied)")
153
+ ok(_exists(cg, "s_kn"), "R7b 拒绝后 knowledge 节点索引与盘面原样保留")
154
+
155
+ # G3:verify 角色 falsified contextual 节点 → 本职可写,照常负记忆化
156
+ r = cg.verify("s_ctx", "verify 角色反例(contextual)", "falsified")
157
+ ok(r.get("action") == "falsified", "G3a verify 角色 falsified contextual 层放行")
158
+ ok(not _exists(cg, "s_ctx"), "G3b contextual 节点按负记忆化移除(语义不变)")
159
+ cg.close()
160
+
161
+ # R8:designer(层写不限)falsified self 层节点仍被 guard_forget 拦截
162
+ cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
163
+ cg.add("s_self", _BODY, layer="self")
164
+ _falsified_must_reject(cg, "s_self", "R8_designer_self")
165
+ cg.close()
166
+
167
+
168
+ def main():
169
+ tmp = tempfile.mkdtemp(prefix="mdcg_n130_guard_")
170
+ try:
171
+ _base_shape(tmp)
172
+ _secure_shape(tmp)
173
+ finally:
174
+ shutil.rmtree(tmp, ignore_errors=True)
175
+
176
+ print("\n" + "=" * 60)
177
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
178
+ if FAILS:
179
+ for f in FAILS:
180
+ print(f" - {f}")
181
+ return 0 if FAIL == 0 else 1
182
+
183
+
184
+ if __name__ == "__main__":
185
+ sys.exit(main())
@@ -0,0 +1,205 @@
1
+ # -*- coding: utf-8 -*-
2
+ """N131 守卫:review_decide(merge) 的目标写入必须过 add 同款双闸。
3
+
4
+ 缺陷形态(2026-09-25 引擎域新发现,编号 N131,high):review_decide 的
5
+ merge 分支(mdcos.py:2221-2255)把提案内容直接 _write_node 追加进任意
6
+ merge_into 目标节点,绕过 guard_write 与 require_layer_write——accept/edit
7
+ 走 self.add()(:2211)有 MdCGSecure.add :3769 require_layer_write 与基类
8
+ add mdcg.py:1311 guard_write 双闸,merge 直写零闸;mcp_server review_decide
9
+ 注释「accept/edit 落节点时的层写仍由下游 require_layer_write 拦截」对 merge
10
+ 不成立,orchestr 角色 forbidden「anchor/self/goals/knowledge 层」
11
+ (tokens.py:123)被打穿。
12
+
13
+ 攻击面:持 review+write op 的令牌(orchestr 的 ops_allow 两者皆含,
14
+ tokens.py:120;designer 全权)——propose 任意内容 → review_decide(pid,
15
+ decision="merge", merge_into=<self/anchor/immutable/knowledge 任意节点 id>)
16
+ 注入任意文本。
17
+
18
+ 修复:merge 分支在 _write_node 之前接与 add 同款双闸——principal 在位先
19
+ require_layer_write(目标层, 目标敏感度)(与 MdCGSecure.add 同序同错型),
20
+ 再 guard_write(self/anchor 层、immutable 标记,对照 add :1311;override
21
+ 显式传参时快照+留痕放行,与 add 能力面对齐);闸在 _record_decision 之前
22
+ 抛出——裁决不落库、提案留 pending(审计节点写入本就在 try/except 内,
23
+ 闸放那里会被吞成 record_error,形同虚设)。
24
+
25
+ 红项 = R1-R4(self/immutable 目标引擎面 + knowledge/self 目标 orchestr 面
26
+ 注入复现);G 项锁语义不变(普通目标 merge 追加、orchestr 本职 contextual
27
+ 目标放行、designer override 放行)+ 两对照(orchestr add self 被拒 /
28
+ designer add 覆 immutable 被拒——闸原本就在,只是 merge 绕过)。
29
+ 运行:python -m md_cg.test_n131_merge_gate
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import os
34
+ import shutil
35
+ import sys
36
+ import tempfile
37
+
38
+ try:
39
+ from . import protect
40
+ from .mdcos import MdCGOS, MdCGSecure
41
+ from .security import AccessDenied, Principal
42
+ except ImportError: # 直接脚本运行
43
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
44
+ from md_cg import protect
45
+ from md_cg.mdcos import MdCGOS, MdCGSecure
46
+ from md_cg.security import AccessDenied, Principal
47
+
48
+ PASS = FAIL = 0
49
+ FAILS = []
50
+
51
+
52
+ def ok(cond, label):
53
+ global PASS, FAIL
54
+ if cond:
55
+ PASS += 1
56
+ print(f" OK {label}")
57
+ else:
58
+ FAIL += 1
59
+ FAILS.append(label)
60
+ print(f" FAIL {label}")
61
+
62
+
63
+ _MARK = "攻击者注入段落"
64
+
65
+
66
+ def _body(name):
67
+ return (f"# 功能名:{name}\n# 生效条件:无条件\n# 子功能:无\n"
68
+ f"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n原始正文。")
69
+
70
+
71
+ def _content_of(cg, nid):
72
+ n = cg.get(nid)
73
+ return (n or {}).get("content") or ""
74
+
75
+
76
+ def _merge_must_reject(cg, pid, target, exc_type, label):
77
+ """merge 进 target:期望 exc_type 且目标内容原样。"""
78
+ raised = None
79
+ try:
80
+ cg.review_decide(pid, "merge", merge_into=target)
81
+ except exc_type as exc:
82
+ raised = exc
83
+ ok(raised is not None, f"{label}1 merge 进 {target} 被拒({exc_type.__name__})")
84
+ ok(_MARK not in _content_of(cg, target),
85
+ f"{label}2 拒绝后 {target} 正文无注入内容")
86
+
87
+
88
+ def _base_shape(tmp):
89
+ """A:基类引擎面(无令牌)——self/immutable 目标不得被 merge 直写。"""
90
+ print("--- A 基类引擎面:guard_write 闸")
91
+ root = os.path.join(tmp, "base")
92
+ cg = MdCGOS(root)
93
+ cg.add("tgt_self", _body("自我目标"), layer="self")
94
+ cg.add("tgt_imm", _body("不可覆盖目标"), layer="knowledge", immutable=True)
95
+ cg.add("tgt_plain", _body("普通目标"), layer="knowledge")
96
+ ok(protect.is_immutable(cg, "tgt_imm")[0],
97
+ "A0 预置生效:tgt_imm 判定为不可覆盖")
98
+ for nid in ("p_self", "p_imm", "p_plain"):
99
+ cg.propose(nid, _body(nid) + "\n" + _MARK + "\n", layer="knowledge")
100
+
101
+ _merge_must_reject(cg, next(r["pid"] for r in cg.review_list()
102
+ if r["id"] == "p_self"),
103
+ "tgt_self", protect.ProtectionError, "R1_self")
104
+ _merge_must_reject(cg, next(r["pid"] for r in cg.review_list()
105
+ if r["id"] == "p_imm"),
106
+ "tgt_imm", protect.ProtectionError, "R2_immutable")
107
+
108
+ # G1:普通目标 merge 照常放行(test_merge_upsert / test_p2 语义不变)
109
+ pid_plain = next(r["pid"] for r in cg.review_list() if r["id"] == "p_plain")
110
+ r = cg.review_decide(pid_plain, "merge", merge_into="tgt_plain")
111
+ ok(r.get("ok") and r.get("node_id") == "tgt_plain",
112
+ "G1a 普通 knowledge 目标 merge 照常放行")
113
+ ok(_MARK in _content_of(cg, "tgt_plain"),
114
+ "G1b 追加内容进入目标正文(merge 本职不变)")
115
+ cg.close()
116
+
117
+
118
+ def _secure_shape(tmp):
119
+ """B:MdCGSecure 面——merge 按目标层做 require_layer_write(orchestr 面)。"""
120
+ print("--- B 安全面:orchestr 层写校验")
121
+ root = os.path.join(tmp, "secure")
122
+ designer_p = Principal(actor="designer-1", role="designer", can_write=True,
123
+ can_admin=True, clearance="secret")
124
+ orch_p = Principal(actor="orch-1", role="orchestr", can_write=True,
125
+ can_admin=False, clearance="internal",
126
+ layers_allow=["contextual", "unresolved", "rejected"],
127
+ ops_allow=["info", "route", "read", "write", "review"])
128
+ cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
129
+ cg.add("t_kn", _body("知识目标"), layer="knowledge")
130
+ cg.add("t_ctx", _body("情境目标"), layer="contextual")
131
+ cg.add("t_self", _body("自我目标"), layer="self")
132
+ cg.close()
133
+
134
+ # 对照 R3:orchestr 直接 add 写 self 层被拒(身份确实受限,闸原本就在)
135
+ cg = MdCGSecure(root, principal=orch_p, master_key=bytes(range(32)))
136
+ denied = None
137
+ try:
138
+ cg.add("x_self", _body("越权"), layer="self")
139
+ except AccessDenied as exc:
140
+ denied = exc
141
+ ok(denied is not None, "R3 对照:orchestr add 写 self 层被拒(AccessDenied)")
142
+
143
+ # R4/R5:orchestr merge 注入 knowledge / self 层目标——旧码双写路径全绕
144
+ #(提案内容须互异:propose 按 payload_hash 幂等去重,同内容返回既有 pid)
145
+ pid_kn = cg.propose("a_kn", _body("提案注入知识") + "\n" + _MARK + "知识\n",
146
+ layer="contextual")
147
+ _merge_must_reject(cg, pid_kn, "t_kn", AccessDenied, "R4_orch_knowledge")
148
+ pid_self = cg.propose("a_self", _body("提案注入自我") + "\n" + _MARK + "自我\n",
149
+ layer="contextual")
150
+ _merge_must_reject(cg, pid_self, "t_self", AccessDenied, "R5_orch_self")
151
+
152
+ # G2:orchestr 本职可写层(contextual)目标 merge 照常放行
153
+ pid_ctx = cg.propose("a_ctx", _body("提案注入情境") + "\n" + _MARK + "情境\n",
154
+ layer="contextual")
155
+ r = cg.review_decide(pid_ctx, "merge", merge_into="t_ctx")
156
+ ok(r.get("ok") and _MARK in _content_of(cg, "t_ctx"),
157
+ "G2 orchestr merge 进本职 contextual 目标放行且内容追加(本职不变)")
158
+ cg.close()
159
+
160
+ # 对照 R6:designer add 覆写 immutable 既有节点被拒(guard_write 对照)+
161
+ # G3:designer merge 带 override 进 immutable 目标显式放行(快照+留痕)
162
+ cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
163
+ cg.add("t_imm2", _body("不可覆盖"), layer="knowledge", immutable=True)
164
+ prot = None
165
+ try:
166
+ cg.add("t_imm2", _body("覆写"), layer="knowledge")
167
+ except protect.ProtectionError as exc:
168
+ prot = exc
169
+ ok(prot is not None, "R6 对照:designer add 覆写 immutable 节点被拒"
170
+ "(ProtectionError)")
171
+ cg.close()
172
+
173
+ cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
174
+ pid_ov = cg.propose("a_ov", _body("提案") + "\n" + _MARK + "\n",
175
+ layer="knowledge")
176
+ r = cg.review_decide(pid_ov, "merge", merge_into="t_imm2", override=True)
177
+ ok(r.get("ok") and _MARK in _content_of(cg, "t_imm2"),
178
+ "G3a designer override=True merge 进 immutable 目标显式放行")
179
+ hist = protect.history(cg, "t_imm2")
180
+ ok(len(hist) >= 1, "G3b override 放行前旧版本已快照 _protected_history")
181
+ audit = os.path.join(cg.root, protect.AUDIT_FILE)
182
+ ok(os.path.exists(audit) and "override_write" in open(
183
+ audit, encoding="utf-8").read(),
184
+ "G3c override_write 动作已留痕 _protected_audit.jsonl")
185
+ cg.close()
186
+
187
+
188
+ def main():
189
+ tmp = tempfile.mkdtemp(prefix="mdcg_n131_guard_")
190
+ try:
191
+ _base_shape(tmp)
192
+ _secure_shape(tmp)
193
+ finally:
194
+ shutil.rmtree(tmp, ignore_errors=True)
195
+
196
+ print("\n" + "=" * 60)
197
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
198
+ if FAILS:
199
+ for f in FAILS:
200
+ print(f" - {f}")
201
+ return 0 if FAIL == 0 else 1
202
+
203
+
204
+ if __name__ == "__main__":
205
+ sys.exit(main())
@@ -0,0 +1,160 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_p1x_ref_root.py · P1-X 回归守卫:op=ref / index_code / index_doc 的根白名单
3
+
4
+ 安全缺陷(两份独立报告合并,severity=high):op=ref 是越权任意文件读原语——
5
+ probe_ref 以调用方自报的 root 与 ref.path 直接 os.path.join 后 open 全文回读
6
+ (refindex.py),无租户根校验、无 check_path_root、无密级闸。三形态全部漏:
7
+
8
+ ① 自报 root 指向记忆根外(victim/secret.txt 全文进模型上下文);
9
+ ② ref.path 绝对路径(os.path.join 遇绝对路径丢弃 root);
10
+ ③ '../' 上跳。
11
+ 同族缺口:index_code / index_doc 的 path 同样不走 check_path_root
12
+ (P1-4 只覆盖 ingest/export/link/mdcg_ingest)。
13
+
14
+ 修复:_ref_call 的 read/get 分支按「最终将打开的完整路径」挂
15
+ check_path_root(MDCG_INGEST_ROOT);index_code/index_doc 对模型可控的显式
16
+ path 挂同闸。语义与 P1-4 先例一致:env 未设置 = 放开(部署开关,默认行为
17
+ 不变);设置了 = realpath 落根内否则拒(fail-closed,上跳与绝对路径在
18
+ realpath 归一后自然涵盖)。
19
+
20
+ 本文件钉死:三形态 + 索引越界必须拒绝;白名单内索引/回读不误伤;
21
+ env 未设保持放开语义;ingest 先例闸与 guest op 闸不回归。
22
+ 运行:python -m md_cg.test_p1x_ref_root
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import os
27
+ import shutil
28
+ import sys
29
+ import tempfile
30
+
31
+ from . import corpus, tokens
32
+ from .mdcg import MdCG
33
+ from .mcp_server import call_tool
34
+ from .security import AccessDenied
35
+
36
+ PASS = FAIL = 0
37
+ FAILS = []
38
+
39
+ SECRET_TEXT = ("AKIAIOSFODNN7EXAMPLE-DUMMY\n" # 哑数据,非真实凭据
40
+ "secret-line-2=dummy\nsecret-line-3=dummy\n")
41
+ ALPHA_TEXT = '""" 供能注释。"""\n\ndef compute(mass):\n return mass\n'
42
+
43
+
44
+ def check(name, cond, detail=""):
45
+ global PASS, FAIL
46
+ if cond:
47
+ PASS += 1
48
+ print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
49
+ else:
50
+ FAIL += 1
51
+ FAILS.append(name)
52
+ print(f" [FAIL] {name} · {detail}")
53
+
54
+
55
+ def denied(fn):
56
+ """断言 fn 被拒:抛 PermissionError,或返回 ok=False。"""
57
+ try:
58
+ out = fn()
59
+ return out.get("ok") is False, f"returned ok={out.get('ok')}"
60
+ except PermissionError as exc:
61
+ return True, f"PermissionError({str(exc)[:40]}…)"
62
+
63
+
64
+ def main():
65
+ print("=" * 68)
66
+ print("md_cg P1-X 验收 · op=ref/index_code/index_doc 根白名单(三形态防越权读)")
67
+ print("=" * 68)
68
+
69
+ tmp = tempfile.mkdtemp(prefix="mdcg_p1x_")
70
+ memroot = os.path.join(tmp, "memroot") # 认知图根(服务器侧)
71
+ allow = os.path.join(tmp, "allow") # 部署白名单内的代码目录
72
+ victim = os.path.join(tmp, "victim") # 白名单外(宿主敏感区模拟)
73
+ os.makedirs(allow)
74
+ os.makedirs(victim)
75
+ with open(os.path.join(victim, "secret.txt"), "w", encoding="utf-8") as f:
76
+ f.write(SECRET_TEXT)
77
+ with open(os.path.join(allow, "alpha.py"), "w", encoding="utf-8") as f:
78
+ f.write(ALPHA_TEXT)
79
+
80
+ env = "MDCG_INGEST_ROOT"
81
+ old = os.environ.get(env)
82
+ os.environ[env] = allow # 部署声明:只有 allow/ 可读入
83
+
84
+ corpus.reset_root(memroot)
85
+ cg = MdCG(memroot)
86
+ full = {"lineno": 1, "end": 99} # 无 hash → 不做漂移校验直读
87
+
88
+ try:
89
+ print("\n【1】三形态越权读必须被拒(fail-closed)")
90
+ ok, d = denied(lambda: call_tool(cg, "cg", {
91
+ "op": "ref", "ref": dict(path="secret.txt", root=victim, **full)}))
92
+ check("①a 自报 root 越界被拒(secret 不进上下文)", ok, d)
93
+ ok, d = denied(lambda: call_tool(cg, "cg", {
94
+ "op": "ref", "root": allow,
95
+ "ref": dict(path=os.path.join(victim, "secret.txt"), **full)}))
96
+ check("①b ref.path 绝对路径被拒(join 丢弃 root 也拦)", ok, d)
97
+ ok, d = denied(lambda: call_tool(cg, "cg", {
98
+ "op": "ref", "root": allow,
99
+ "ref": dict(path=os.path.join("..", "victim", "secret.txt"),
100
+ **full)}))
101
+ check("①c '../' 上跳被拒", ok, d)
102
+
103
+ print("\n【2】同族:index_code / index_doc 越界 path 必须被拒")
104
+ ok, d = denied(lambda: call_tool(cg, "cg", {"op": "index_code",
105
+ "path": victim}))
106
+ check("②a index_code 越界 path 被拒", ok, d)
107
+ ok, d = denied(lambda: call_tool(cg, "cg", {"op": "index_doc",
108
+ "path": victim}))
109
+ check("②b index_doc 越界 path 被拒", ok, d)
110
+
111
+ print("\n【3】白名单内不误伤")
112
+ out = call_tool(cg, "cg", {"op": "index_code", "path": allow})
113
+ nid = (out.get("ids") or [None])[0]
114
+ rr = call_tool(cg, "cg", {"op": "ref", "node_id": nid}) if nid \
115
+ else {"ok": False, "error": "no ids"}
116
+ check("③a 白名单内索引 + node 回读正常",
117
+ out.get("ok") and out.get("indexed", 0) >= 1
118
+ and rr.get("ok") is True and "compute" in (rr.get("text") or ""),
119
+ f"indexed={out.get('indexed')} ref_ok={rr.get('ok')}")
120
+ rr = call_tool(cg, "cg", {"op": "ref", "root": allow,
121
+ "ref": dict(path="alpha.py", lineno=1, end=2)})
122
+ check("③b 白名单内 inline ref 回读正常", rr.get("ok") is True,
123
+ f"ok={rr.get('ok')} err={rr.get('error')}")
124
+
125
+ print("\n【4】先例与开关语义不回归")
126
+ ok, d = denied(lambda: call_tool(cg, "cg", {
127
+ "op": "ingest", "action": "file",
128
+ "path": os.path.join(victim, "secret.txt")}))
129
+ check("④a ingest 越界仍被 P1-4 拦(先例闸不回归)", ok, d)
130
+ os.environ.pop(env, None)
131
+ rr = call_tool(cg, "cg", {
132
+ "op": "ref", "ref": dict(path="secret.txt", root=victim, **full)})
133
+ check("④b env 未设 = 部署开关放开(默认行为不变)",
134
+ rr.get("ok") is True
135
+ and "AKIAIOSFODNN7EXAMPLE" in (rr.get("text") or ""),
136
+ f"ok={rr.get('ok')}")
137
+ try:
138
+ tokens.Principal(role="guest", ops_allow=("read",), theory_ok=True
139
+ ).require_op("ref")
140
+ ok, d = False, "guest 未被拒"
141
+ except AccessDenied:
142
+ ok, d = True, "AccessDenied"
143
+ check("④c guest 令牌 op=ref 仍被 require_op 拒", ok, d)
144
+ finally:
145
+ if old is None:
146
+ os.environ.pop(env, None)
147
+ else:
148
+ os.environ[env] = old
149
+ corpus.reset_root(memroot) # 收尾:指向本次临时根
150
+ shutil.rmtree(tmp, ignore_errors=True)
151
+
152
+ print("\n" + "=" * 68)
153
+ print(f"P1-X 根白名单守卫: {PASS}/{PASS + FAIL} 通过")
154
+ if FAILS:
155
+ print("未过:" + ";".join(FAILS))
156
+ return 0 if FAIL == 0 else 1
157
+
158
+
159
+ if __name__ == "__main__":
160
+ sys.exit(main())