@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,277 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """读权限分级守卫(批次 27)。
4
+
5
+ 分级语义:
6
+ - 设计者/主代理:read_file 全路径(2026-09-19 裁定),文本内容过 PII 脱敏
7
+ (跳过个人敏感信息不入明文);
8
+ - 子代理(worker):read_file 收敛到工作区(job_dir)+ 定制工作目录
9
+ (spec.workdir / spec.read_roots)——其他会话与越界内容拒读(fail-closed,
10
+ 无根即无文件读权限);lingshu_cg 读面默认密级 internal——**错误处置标记
11
+ (private,错误相关/待排查内容限制平级扩散,非个人隐私)与 secret 拒读**。
12
+ 错误处置链路(设计者/上级节点/验证单元)必读 private 不受此限——verify
13
+ 面当前 clearance_cap=internal 的缺口已登记(安全审计实锚文档)。
14
+ """
15
+ import importlib.util
16
+ import os
17
+ import sys
18
+ import tempfile
19
+
20
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
+
23
+ PASS = 0
24
+ FAIL = 0
25
+ FAILS = []
26
+
27
+
28
+ def check(name, cond, detail=""):
29
+ global PASS, FAIL
30
+ if cond:
31
+ PASS += 1
32
+ print(f" OK {name}")
33
+ else:
34
+ FAIL += 1
35
+ FAILS.append(name)
36
+ print(f" FAIL {name} {detail}")
37
+
38
+
39
+ def _load_exec():
40
+ spec = importlib.util.spec_from_file_location(
41
+ "hx_b27", os.path.join(REPO, "hive", "exec.py"))
42
+ m = importlib.util.module_from_spec(spec)
43
+ try:
44
+ spec.loader.exec_module(m)
45
+ except SystemExit:
46
+ pass
47
+ return m
48
+
49
+
50
+ def main():
51
+ hx = _load_exec()
52
+
53
+ print("== _worker_scope 白名单构造 ==")
54
+ jd = tempfile.mkdtemp(prefix="b27_job_")
55
+ wd = tempfile.mkdtemp(prefix="b27_cust_")
56
+ extra = tempfile.mkdtemp(prefix="b27_extra_")
57
+ scope = hx._worker_scope(jd, {"workdir": wd, "read_roots": [extra, extra]})
58
+ check("B27-1a 工作区+定制目录进白名单(去重)",
59
+ scope == (os.path.realpath(jd), os.path.realpath(wd),
60
+ os.path.realpath(extra)), str(scope))
61
+ check("B27-1b 无根 worker 得空 tuple",
62
+ hx._worker_scope(None, {}) == ())
63
+
64
+ print("== read_file workspace 收敛(fail-closed)==")
65
+ f_in = os.path.join(jd, "in_scope.txt")
66
+ with open(f_in, "w", encoding="utf-8") as f:
67
+ f.write("工作区内文件 alpha@test.com 13812345678")
68
+ other = tempfile.mkdtemp(prefix="b27_other_sess_")
69
+ f_out = os.path.join(other, "other_session.md")
70
+ with open(f_out, "w", encoding="utf-8") as f:
71
+ f.write("其他会话内容")
72
+ r = hx.tool_read_file({"path": f_in}, workdir=jd, scope_roots=scope)
73
+ check("B27-2a 工作区内可读", r.get("ok") is True, str(r)[:120])
74
+ check("B27-2b 内容 PII 已脱敏(邮箱+手机号)",
75
+ "alpha@test.com" not in (r.get("content") or "")
76
+ and "13812345678" not in (r.get("content") or "")
77
+ and "[已脱敏:邮箱]" in r.get("content")
78
+ and "[已脱敏:手机号]" in r.get("content"), str(r.get("content"))[:80])
79
+ check("B27-2c pii_redacted 标记", r.get("pii_redacted") is True)
80
+ r = hx.tool_read_file({"path": f_out}, workdir=jd, scope_roots=scope)
81
+ check("B27-2d 其他会话目录拒读",
82
+ r.get("ok") is False and "scope_roots" in r, str(r)[:120])
83
+ r = hx.tool_read_file({"path": f_in}, workdir=jd, scope_roots=())
84
+ check("B27-2e 空白名单(无根 worker)拒读", r.get("ok") is False)
85
+ r = hx.tool_read_file({"path": f_in}, workdir=jd)
86
+ check("B27-2f 主代理(scope_roots=None)现状放开", r.get("ok") is True)
87
+
88
+ print("== PII 脱敏模式集 ==")
89
+ # 样本运行时拼接(R1 凭据扫描只认字面量——样本非真凭据)
90
+ _sk = "sk-" + "abcdefghijklmnopq123456"
91
+ _pk = ("-----BEGIN " + "RSA PRIVATE KEY-----\nMIIabc\n"
92
+ "-----END " + "RSA PRIVATE KEY-----")
93
+ _bearer = "Bearer " + "eyJhbGciOiJIUzI1NiIsIntoken"
94
+ cases = [
95
+ ("私钥块", _pk),
96
+ ("API密钥", "my key: " + _sk),
97
+ ("API密钥", "Authorization: " + _bearer),
98
+ ("身份证号", "身份证 110101199003078515"),
99
+ ("邮箱", "contact user@example.com here"),
100
+ ("手机号", "电话 13912345678"),
101
+ ]
102
+ for label, sample in cases:
103
+ out = hx._redact_pii(sample)
104
+ check(f"B27-3 {label} 脱敏", "[已脱敏:" in out and sample not in out,
105
+ out[:60])
106
+ plain = "普通技术文档:quick sort 复杂度 O(n log n),蜂群调度正常。"
107
+ check("B27-3x 普通文本不误伤", hx._redact_pii(plain) == plain)
108
+
109
+ print("== lingshu_cg 密级分级(worker=internal 拒读 private/secret)==")
110
+ from md_cg.mdcos import MdCGSecure
111
+ from md_cg.security import Principal
112
+ root = tempfile.mkdtemp(prefix="b27_cg_")
113
+ cg_admin = MdCGSecure(root, principal=Principal(
114
+ actor="seed", clearance="secret", can_write=True, can_admin=True,
115
+ role="designer", auth_mode="test"))
116
+ cg_admin.add("pub_node", "# 功能名:公开\n# 正文:公开内容 alpha",
117
+ layer="knowledge", sensitivity="public")
118
+ cg_admin.add("priv_node", "# 功能名:私有\n# 正文:标记私有内容 beta",
119
+ layer="knowledge", sensitivity="private")
120
+ cg_admin.flush()
121
+ worker = Principal(actor="hive-worker", clearance="internal",
122
+ can_write=True, can_admin=False, role="recorder",
123
+ auth_mode="hive-exec")
124
+ cg_w = MdCGSecure(root, principal=worker)
125
+ r_pub, _ = cg_w.search("公开", k=5)
126
+ r_priv, _ = cg_w.search("标记私有", k=5)
127
+ check("B27-4a worker 可读 internal 以下节点",
128
+ any(x[0].get("id") == "pub_node" for x in r_pub),
129
+ str([x[0].get("id") for x in r_pub]))
130
+ check("B27-4b worker 读不到 private(标记私有)节点",
131
+ not any(x[0].get("id") == "priv_node" for x in r_priv),
132
+ str([x[0].get("id") for x in r_priv]))
133
+ try:
134
+ got = cg_w.get("priv_node") is not None # None = 密级过滤不可得
135
+ except Exception: # noqa: BLE001——AccessDenied 也算拒
136
+ got = False
137
+ check("B27-4c worker get private 节点被拒/不可得", not got)
138
+
139
+ print("== verify 面错误处置豁免(批次 28 裁定落地)==")
140
+ from md_cg.tokens import issue, role_spec, verify_token
141
+ check("B27-5a verify clearance_cap=internal(分型后 cap 豁免取消,"
142
+ "restricted 走链路角色集)",
143
+ role_spec("verify")["clearance_cap"] == "internal")
144
+ check("B27-5b record 维持限读(cap=internal)",
145
+ role_spec("record")["clearance_cap"] == "internal")
146
+ tfile = os.path.join(tempfile.mkdtemp(prefix="b27_tok_"), "tok.json")
147
+ issued = issue("verify", actor="b27-verify", path=tfile)
148
+ check("B27-5c verify 签发 clearance=internal(分型取代 cap 豁免)",
149
+ issued["clearance"] == "internal", str(issued)[:120])
150
+ vp = verify_token(issued["token"], path=tfile)
151
+ check("B27-5d verify_token 解析 clearance=internal",
152
+ vp.clearance == "internal", str(vp))
153
+ cg_v = MdCGSecure(root, principal=vp)
154
+ got_priv = cg_v.get("priv_node")
155
+ # 现状断言(批次 28):密级闸已放行(cap=private),但 private 节点受
156
+ # **信封加密**第二层限制——provision_dek 为每个 actor 发独立 DEK,节点
157
+ # 用写入者 DEK 加密,跨 actor(含 verify)解密失败静默空。跨 actor 读
158
+ # 需 DEK 共享机制(三选项已登记安全审计实锚文档),本批只完成密级闸面。
159
+ check("B27-5e verify 密级闸放行(跨 actor 解密留 DEK 共享专项)",
160
+ got_priv is None, str(got_priv)[:100])
161
+ r_secret, _ = cg_v.search("标记私有", k=5)
162
+ _ = r_secret
163
+ # secret 仍禁:构造 secret 节点验证不可得
164
+ cg_admin.add("sec_node", "# 功能名:最高密\n# 正文:secret 内容样本",
165
+ layer="knowledge", sensitivity="secret")
166
+ cg_admin.flush()
167
+ got_secret = cg_v.get("sec_node")
168
+ check("B27-5f verify 读 secret 节点仍拒",
169
+ got_secret is None, str(got_secret)[:80])
170
+ rec_p = Principal(actor="b27-rec", clearance="internal",
171
+ can_write=True, can_admin=False, role="recorder",
172
+ auth_mode="test")
173
+ cg_r = MdCGSecure(root, principal=rec_p)
174
+ check("B27-5g record(cap internal)读 private 仍拒",
175
+ cg_r.get("priv_node") is None)
176
+
177
+ print("== 批次 28 分型:restricted 错误处置标记可见性矩阵 ==")
178
+ root_r = tempfile.mkdtemp(prefix="b27_res_")
179
+ cg_seed = MdCGSecure(root_r, principal=Principal(
180
+ actor="seed", clearance="secret", can_write=True, can_admin=True,
181
+ role="designer", auth_mode="test"))
182
+ cg_seed.add("err_node", "# 功能名:错误处置样本\n# 正文:错误相关内容 gamma",
183
+ layer="knowledge", sensitivity="restricted")
184
+ cg_seed.add("priv_node2", "# 功能名:隐私样本\n# 正文:隐私内容 delta",
185
+ layer="knowledge", sensitivity="private")
186
+ cg_seed.flush()
187
+
188
+ def reader(role, clearance, can_admin=False):
189
+ p = Principal(actor="r-" + role, clearance=clearance,
190
+ can_write=False, can_admin=can_admin, role=role,
191
+ auth_mode="test")
192
+ return MdCGSecure(root_r, principal=p)
193
+
194
+ got = reader("designer", "secret", can_admin=True).get("err_node")
195
+ check("B27-6a designer 读 restricted 可(处置链路)",
196
+ got is not None and "gamma" in (got.get("content") or ""))
197
+ got = reader("orchestr", "internal").get("err_node")
198
+ check("B27-6b orchestr(上级节点)读 restricted 可",
199
+ got is not None and "gamma" in (got.get("content") or ""))
200
+ got = reader("verify", "internal").get("err_node")
201
+ check("B27-6c verify 读 restricted 可(验证本职)",
202
+ got is not None and "gamma" in (got.get("content") or ""))
203
+ for role, cl in (("record", "internal"), ("output", "internal")):
204
+ got = reader(role, cl).get("err_node")
205
+ check("B27-6d " + role + "(平级/下游)读 restricted 拒", got is None)
206
+ got = reader("recorder", "internal").get("err_node")
207
+ check("B27-6e worker(recorder)读 restricted 拒", got is None)
208
+ guest_cg = MdCGSecure(root_r)
209
+ got = guest_cg.get("err_node")
210
+ check("B27-6f guest 读 restricted 拒", got is None)
211
+ import glob as _glob
212
+ _files = _glob.glob(os.path.join(root_r, "knowledge", "**",
213
+ "err_node.md"), recursive=True)
214
+ raw = open(_files[0], encoding="utf-8").read() if _files else ""
215
+ check("B27-6g restricted 落盘不加密(明文可读)", "gamma" in raw)
216
+ got = reader("verify", "internal").get("priv_node2")
217
+ check("B27-6h private 会话绑定语义不变(非归属拒)", got is None)
218
+ from md_cg.security import SENSITIVITY_ORDER as _SO
219
+ check("B27-6i 阶梯 public<internal<restricted<private<secret",
220
+ _SO == ("public", "internal", "restricted", "private", "secret"))
221
+
222
+ print("== 批次 30:安壮组回归断言(P2-15/16/17/18/19/20/24)==")
223
+ # P2-16:units 判活 timeout 存在
224
+ u = open(os.path.join(REPO, "md_cg", "units.py"), encoding="utf-8").read()
225
+ check("B30-1 units tasklist timeout=10",
226
+ "errors=\"replace\", timeout=10" in u)
227
+ # P2-17:exec 响应上限
228
+ hx_src = open(os.path.join(REPO, "hive", "exec.py"),
229
+ encoding="utf-8").read()
230
+ check("B30-2 resp.read(RESP_MAX_BYTES) 全覆盖",
231
+ "RESP_MAX_BYTES" in hx_src
232
+ and "resp.read().decode" not in hx_src)
233
+ # P2-18:payload test_cmd 注入点移除
234
+ au = open(os.path.join(REPO, "md_cg", "audit.py"),
235
+ encoding="utf-8").read()
236
+ check("B30-3 audit test_cmd 只走 env",
237
+ 'payload.get("test_cmd")' not in au
238
+ and 'os.environ.get("MDCG_CODE_TEST_CMD")' in au)
239
+ # P2-19:外部验证器模块名白名单 + 加载 stderr 可见
240
+ check("B30-4 verifier 模块名白名单",
241
+ "_RESTRICTED" not in au and "A-Za-z_" in au
242
+ and "已加载外部验证器模块" in au)
243
+ # P2-20:读路径根校验单点
244
+ mg = open(os.path.join(REPO, "md_cg", "mdcg.py"),
245
+ encoding="utf-8").read()
246
+ check("B30-5 _node_disk_path 单点 + 旧 join 清零",
247
+ "_node_disk_path" in mg
248
+ and 'os.path.join(self.root, e["path"])' not in mg)
249
+ # P2-24:watchdog creationflags 跨平台
250
+ bw = open(os.path.join(REPO, "scripts", "bootstrap_watchdog.py"),
251
+ encoding="utf-8").read()
252
+ check("B30-6 creationflags 笔误修正 + start_new_session",
253
+ "start_new_session=not _nt" in bw
254
+ and "creationflags=subprocess.DEVNULL" not in bw)
255
+ # P2-15:bootstrap_loop/llm_channel 无裸 open().read/dump
256
+ bl = open(os.path.join(REPO, "scripts", "bootstrap_loop.py"),
257
+ encoding="utf-8").read()
258
+ lc = open(os.path.join(REPO, "scripts", "llm_channel.py"),
259
+ encoding="utf-8").read()
260
+ check("B30-7 bootstrap_loop 无裸 json.load(open(",
261
+ "json.load(open(" not in bl)
262
+ check("B30-8 llm_channel 上下文管理器",
263
+ "json.load(open(" not in lc)
264
+ # 行为断言:_readable restricted 矩阵已在 B27-6 覆盖;此处补
265
+ # P2-17 行为(RESP_MAX_BYTES 常量值合理)
266
+ check("B30-9 RESP_MAX_BYTES=8MB",
267
+ "RESP_MAX_BYTES = 8 * 1024 * 1024" in hx_src)
268
+
269
+ print("\n" + "=" * 60)
270
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
271
+ for x in FAILS:
272
+ print(f" - {x}")
273
+ return 0 if FAIL == 0 else 1
274
+
275
+
276
+ if __name__ == "__main__":
277
+ sys.exit(main())
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """检索读缓存默认开启守卫(issue #31 P2 后续:生产入口 O(n) 全池扫描修复)。
4
+
5
+ 背景(缺陷报告实测):生产检索入口(mcp_server mdcg_search → MdCGSecure.
6
+ (MdCGOS).search → _read_many,mdcos.py:841)默认态每查询把全池节点文件
7
+ 逐个 open+2×realpath+frontmatter 解析——3300 节点池实测中位 ~602ms/查询、
8
+ 万级外推 ~2s+,线性 O(n)。读缓存(readcache.install)早已存在但默认关
9
+ (零变更纪律),生产恒不装配——「开关已存在,只差默认值」。
10
+
11
+ 本守卫断言(调用计数/复杂度形态,非时间阈值):
12
+ D1 默认装配:env 未设 MDCG_READ_CACHE → 构造即装(hasattr _read_cache);
13
+ D2 二次检索零重解析:默认态下第二次 search 期间 nodefile.loads 调用数为 0
14
+ (旧默认:=候选池节点数,每查询全量重读 → 本断言红);
15
+ D3 装载恰一次:同实例 3 次检索累计 loads ≤ 池节点数(复杂度 O(装载1次),
16
+ 不是 O(查询数×池));
17
+ D4 opt-out 退出阀:MDCG_READ_CACHE=0 → 无 _read_cache 属性、检索照常;
18
+ D5 默认态写后不陈旧(哨兵失效仍生效):写新节点 → 检索命中新内容;
19
+ D6 结果一致:默认开与显式关(=0)同库同 query 结果 id 序一致(缓存只改
20
+ 速度不改结果)。
21
+ """
22
+ import os
23
+ import shutil
24
+ import sys
25
+ import tempfile
26
+
27
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
+
29
+ from md_cg import nodefile # noqa: E402
30
+ from md_cg.mdcos import MdCGSecure # noqa: E402
31
+ from md_cg.security import Principal # noqa: E402
32
+
33
+ PASS = 0
34
+ FAIL = 0
35
+ FAILS = []
36
+
37
+
38
+ def check(name, cond, detail=""):
39
+ global PASS, FAIL
40
+ if cond:
41
+ PASS += 1
42
+ print(f" OK {name}")
43
+ else:
44
+ FAIL += 1
45
+ FAILS.append(name)
46
+ print(f" FAIL {name} {detail}")
47
+
48
+
49
+ def build_lib(root, n=12):
50
+ cg = MdCGSecure(root, principal=Principal(
51
+ actor="rd", clearance="secret", can_write=True,
52
+ role="designer", auth_mode="test"))
53
+ for i in range(n):
54
+ cg.add("mem_%03d" % i,
55
+ "# 功能名:默认缓存样本 %d\n# 正文:蜂群调度 节点 %d" % (i, i),
56
+ layer="knowledge")
57
+ cg.flush()
58
+ return cg
59
+
60
+
61
+ def count_loads(fn):
62
+ """打桩 nodefile.loads 计数(节点文件解析的唯一入口,_read 内调用)。"""
63
+ n = 0
64
+ real = nodefile.loads
65
+
66
+ def _spy(*a, **kw):
67
+ nonlocal n
68
+ n += 1
69
+ return real(*a, **kw)
70
+
71
+ nodefile.loads = _spy
72
+ try:
73
+ return fn(), n
74
+ finally:
75
+ nodefile.loads = real
76
+
77
+
78
+ def main():
79
+ root = tempfile.mkdtemp(prefix="mdcg_readcache_def_")
80
+ try:
81
+ # D1 默认装配(env 未设 → 默认开)
82
+ print("== D1 默认装配 ==")
83
+ os.environ.pop("MDCG_READ_CACHE", None)
84
+ cg = build_lib(root)
85
+ check("D1a env 未设时构造即装读缓存(hasattr _read_cache)",
86
+ hasattr(cg, "_read_cache"))
87
+ pool_n = len(cg.index["nodes"])
88
+ check("D1b 库就绪(12 节点)", pool_n == 12, f"nodes={pool_n}")
89
+
90
+ # D2/D3 装载复杂度(计数断言,能红 O(查询×池) 的旧默认态)
91
+ print("== D2/D3 装载恰一次 / 二次检索零重解析 ==")
92
+ total = 0
93
+ _, n1 = count_loads(lambda: cg.search("蜂群调度"))
94
+ total += n1
95
+ check("D3a 首次检索装载 ≤ 池节点数", n1 <= pool_n,
96
+ f"first={n1} pool={pool_n}")
97
+ _, n2 = count_loads(lambda: cg.search("蜂群调度"))
98
+ total += n2
99
+ check("D2a 二次检索零重解析(loads==0)", n2 == 0, f"second={n2}")
100
+ _, n3 = count_loads(lambda: cg.search("样本 节点"))
101
+ total += n3
102
+ check("D3b 3 次检索累计装载 ≤ 池节点数(O(装载1次),旧默认为 3×池)",
103
+ total <= pool_n, f"total={total} pool={pool_n}")
104
+
105
+ # D5 默认态写后不陈旧(哨兵失效在默认开下仍生效)
106
+ print("== D5 写后失效 ==")
107
+ cg.add("mem_new", "# 功能名:写入后的新内容BBB\n# 正文:更新后内容BBB 独有标记",
108
+ layer="knowledge")
109
+ cg.flush()
110
+ r5, _ = cg.search("更新后内容BBB")
111
+ check("D5a 写入后新内容立即可检索(不陈旧)",
112
+ any(x[0].get("id") == "mem_new" for x in r5),
113
+ f"ids={[x[0].get('id') for x in r5][:5]}")
114
+
115
+ # D6 结果一致(默认开 vs 显式关):同库同 query 的 id 序一致
116
+ print("== D6 开/关结果一致 ==")
117
+ r_on, _ = cg.search("蜂群调度")
118
+
119
+ os.environ["MDCG_READ_CACHE"] = "0"
120
+ try:
121
+ # 关态独立实例(同库——节点已在盘上,构造扫描即装载索引)
122
+ cg0 = MdCGSecure(root, principal=Principal(
123
+ actor="rd", clearance="secret", can_write=True,
124
+ role="designer", auth_mode="test"))
125
+ r_off, _ = cg0.search("蜂群调度")
126
+ ids_on = [x[0].get("id") for x in r_on]
127
+ ids_off = [x[0].get("id") for x in r_off]
128
+ check("D6a 开/关同库同 query 结果 id 序一致",
129
+ ids_on == ids_off,
130
+ f"on={ids_on[:5]} off={ids_off[:5]}")
131
+ check("D6b 关态退出阀生效(无 _read_cache 属性)",
132
+ not hasattr(cg0, "_read_cache"))
133
+ finally:
134
+ os.environ.pop("MDCG_READ_CACHE", None)
135
+
136
+ # D4 opt-out 退出阀(独立段,防顺序依赖)
137
+ print("== D4 opt-out 退出阀 ==")
138
+ os.environ["MDCG_READ_CACHE"] = "0"
139
+ try:
140
+ root4 = os.path.join(root, "optout")
141
+ os.makedirs(root4, exist_ok=True)
142
+ cg4 = MdCGSecure(root4, principal=Principal(
143
+ actor="rd", clearance="secret", can_write=True,
144
+ role="designer", auth_mode="test"))
145
+ cg4.add("m1", "# 功能名:a\n# 正文:蜂群调度 内容", layer="knowledge")
146
+ cg4.flush()
147
+ check("D4a MDCG_READ_CACHE=0 → 未装缓存(零变更退出阀)",
148
+ not hasattr(cg4, "_read_cache"))
149
+ cg4.search("蜂群调度") # 预热(无缓存,恒读盘)
150
+ _, n4 = count_loads(lambda: cg4.search("蜂群调度"))
151
+ check("D4b 关态二次检索仍逐节点重解析(loads>0,证明退出阀真关)",
152
+ n4 > 0, f"loads={n4}")
153
+ finally:
154
+ os.environ.pop("MDCG_READ_CACHE", None)
155
+ finally:
156
+ os.environ.pop("MDCG_READ_CACHE", None)
157
+ shutil.rmtree(root, ignore_errors=True)
158
+
159
+ print("\n" + "=" * 60)
160
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
161
+ if FAILS:
162
+ for f in FAILS:
163
+ print(f" - {f}")
164
+ return 0 if FAIL == 0 else 1
165
+
166
+
167
+ if __name__ == "__main__":
168
+ sys.exit(main())