@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.
- package/README.md +552 -465
- package/docs/README.md +1 -0
- package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
- package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
- package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
- 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
- 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
- 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
- package/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/index.js +7 -1
- package/lib/lib/roleplay_web.js +530 -443
- package/lib/lib/token_store.d.ts +7 -1
- package/lib/lib/token_store.js +12 -3
- package/md_cg/audit.py +12 -1
- package/md_cg/backfill.py +16 -15
- package/md_cg/backfill_bucket_zh.py +35 -0
- package/md_cg/bench_e2e_judge.py +532 -0
- package/md_cg/bench_e2e_locomo_qa.py +368 -0
- package/md_cg/bench_e2e_qa.py +256 -0
- package/md_cg/branches.py +18 -2
- package/md_cg/ccgc.py +3 -2
- package/md_cg/chain.py +19 -4
- package/md_cg/consolidate.py +7 -6
- package/md_cg/crosscheck.py +4 -3
- package/md_cg/crypto.py +439 -437
- package/md_cg/datapath.py +395 -335
- package/md_cg/evidence.py +582 -580
- package/md_cg/export.py +3 -1
- package/md_cg/forgetting.py +2 -2
- package/md_cg/fsutil.py +48 -0
- package/md_cg/hotcache.py +255 -238
- package/md_cg/interop.py +161 -22
- package/md_cg/judgment_manifest.py +177 -0
- package/md_cg/links.py +655 -622
- package/md_cg/mcp_server.py +280 -29
- package/md_cg/mdcg.py +616 -125
- package/md_cg/mdcos.py +430 -66
- package/md_cg/mreview/govern.py +5 -4
- package/md_cg/postings.py +4 -2
- package/md_cg/readcache.py +76 -18
- package/md_cg/reconcile.py +228 -0
- package/md_cg/review_cli.py +170 -0
- package/md_cg/routing.py +28 -0
- package/md_cg/run_tests.py +211 -0
- package/md_cg/scrub.py +862 -852
- package/md_cg/security.py +385 -275
- package/md_cg/selfreport.py +3 -2
- package/md_cg/signer.py +565 -562
- package/md_cg/sources.py +3 -2
- package/md_cg/stg.py +6 -0
- package/md_cg/sustain.py +1168 -1138
- package/md_cg/test_access_hints.py +147 -0
- package/md_cg/test_branch_discard_tombstone.py +136 -0
- package/md_cg/test_branches.py +259 -249
- package/md_cg/test_chain_read_isolate.py +168 -0
- package/md_cg/test_datapath_device_name.py +203 -0
- package/md_cg/test_emit_negtail_cache.py +156 -0
- package/md_cg/test_en_pipeline.py +186 -166
- package/md_cg/test_govern_directread.py +421 -0
- package/md_cg/test_i32_hotcache_env_key.py +218 -0
- package/md_cg/test_identity_attribution.py +228 -147
- package/md_cg/test_index_durability.py +238 -224
- package/md_cg/test_interop.py +4 -2
- package/md_cg/test_interop_judgment.py +228 -0
- package/md_cg/test_issue39_utf8_stdio.py +273 -0
- package/md_cg/test_links_concurrent_write.py +188 -0
- package/md_cg/test_merge_upsert.py +168 -0
- package/md_cg/test_n123_derive_expiry_chain.py +205 -0
- package/md_cg/test_n130_verify_falsified_protect.py +185 -0
- package/md_cg/test_n131_merge_gate.py +205 -0
- package/md_cg/test_p1x_ref_root.py +160 -0
- package/md_cg/test_p27_docindex.py +774 -765
- package/md_cg/test_p2_mcp.py +3 -0
- package/md_cg/test_p32_backfill.py +304 -298
- package/md_cg/test_p39_verify_flow.py +90 -50
- package/md_cg/test_p47_session_view.py +60 -25
- package/md_cg/test_propose_tail_index.py +157 -0
- package/md_cg/test_read_scope_b27.py +277 -0
- package/md_cg/test_readcache_default_on.py +168 -0
- package/md_cg/test_readcache_precise_inval.py +270 -0
- package/md_cg/test_readcache_prodpath.py +55 -7
- package/md_cg/test_reconcile_v0.py +294 -0
- package/md_cg/test_retr_s1.py +6 -2
- package/md_cg/test_retr_s1b.py +67 -0
- package/md_cg/test_retr_s7.py +8 -0
- package/md_cg/test_retr_s9_entity_ctx.py +181 -175
- package/md_cg/test_retr_score_once.py +208 -0
- package/md_cg/test_review_onepass.py +170 -0
- package/md_cg/test_rrf_graph_seed_cache.py +154 -0
- package/md_cg/test_security_audit.py +155 -0
- package/md_cg/test_security_audit_b26.py +161 -0
- package/md_cg/test_security_audit_v21.py +250 -0
- package/md_cg/test_semantic_canonical.py +255 -241
- package/md_cg/test_session_isolation.py +168 -0
- package/md_cg/test_snapshot_autoclose.py +187 -0
- package/md_cg/test_tail_watermark_race.py +208 -0
- package/md_cg/test_tenant_env_override_warn.py +139 -0
- package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
- package/md_cg/test_v14_fixes.py +415 -397
- package/md_cg/test_verify_dirty_reconcile.py +157 -0
- package/md_cg/theory.py +276 -273
- package/md_cg/tokens.py +734 -677
- package/md_cg/units.py +3 -2
- package/md_cg/vision_evidence.py +4 -3
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
- package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
- package/md_cg/writepipe.py +554 -550
- package/package.json +6 -2
- package/src/bridge.ts +434 -401
- package/src/index.ts +526 -518
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""生产 search T2/T3 重打分守卫(v7 N51 留档复测·修复,缺陷迭代第 14 轮)。
|
|
4
|
+
|
|
5
|
+
缺陷形态(缺陷报告/v9.md:111 留档,600 节点临时库实测逐位复现):
|
|
6
|
+
生产 MdCGOS.search 的 try_stage(mdcos.py)无 scored 形参——T2/T3 均为
|
|
7
|
+
「打分截断后又对 picked 重打」:cut_by_relevance(hits, self._score(hits))
|
|
8
|
+
之后 try_stage(hits) 内部再 _score 一遍;T3 再对 picked 重打。实测无交集
|
|
9
|
+
查询 _score 恰 4 调、per-call docs=[0,0,600,500]、共打分 1100 文档——
|
|
10
|
+
500/1100≈45% 打分工作量冗余(GLOBAL_CAP=500)。基类 try_stage 早有
|
|
11
|
+
scored=None 形参(mdcg.py P2-4,批次 31,reach 分支已用)未回移生产覆写。
|
|
12
|
+
|
|
13
|
+
修复口径:生产 try_stage 照基类 P2-4 模板补 scored=None 形参;T2 hits 腿
|
|
14
|
+
(截断打分结果经 doc_key 键表回填)与 T3 picked 腿、reach hits_r 腿一并
|
|
15
|
+
透传——同一 _score 纯函数(单次 search 内 docs/q/qb/pool_cfg 均不变),
|
|
16
|
+
透传不改变任何分值与输出。
|
|
17
|
+
|
|
18
|
+
本守卫(调用计数/复杂度断言形态,非时间阈值):
|
|
19
|
+
T3 段(无交集查询,600 节点):_score 恰 2 调、per-call [0,600]、共 600
|
|
20
|
+
文档(旧实现 4 调 [0,0,600,500]=1100 → 红);tier/scanned/结果规模
|
|
21
|
+
不变;
|
|
22
|
+
T2 段(有交集查询):_score 恰 1 调、共 600 文档(旧实现 2 调
|
|
23
|
+
[600,500]=1100 → 红);
|
|
24
|
+
纯度段:对计数包装记录的每笔 (docs, scored) 复算 _score 逐位相等
|
|
25
|
+
(透传值 ≡ 现算值——不改结果的直接证据);
|
|
26
|
+
确定性段:同查询连跑两轮结果 id 序与分值逐位一致。
|
|
27
|
+
|
|
28
|
+
运行:python -m md_cg.test_retr_score_once
|
|
29
|
+
"""
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
|
|
35
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
36
|
+
|
|
37
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
38
|
+
|
|
39
|
+
PASS = 0
|
|
40
|
+
FAIL = 0
|
|
41
|
+
FAILS = []
|
|
42
|
+
|
|
43
|
+
N = 600
|
|
44
|
+
QUERY_MISS = "完全无交集查询词项甲乙丙" # LIKE 零命中 → T3 全量兜底
|
|
45
|
+
QUERY_HIT = "蜂群调度" # 有交集 → T2 命中
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check(name, cond, detail=""):
|
|
49
|
+
global PASS, FAIL
|
|
50
|
+
if cond:
|
|
51
|
+
PASS += 1
|
|
52
|
+
print(f" OK {name}")
|
|
53
|
+
else:
|
|
54
|
+
FAIL += 1
|
|
55
|
+
FAILS.append(name)
|
|
56
|
+
print(f" FAIL {name} {detail}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_lib(root):
|
|
60
|
+
cg = MdCGOS(root)
|
|
61
|
+
for i in range(N):
|
|
62
|
+
cg.add("mem_%04d" % i,
|
|
63
|
+
"# 功能名:打分样本 %d\n# 正文:%s 节点 %d" % (i, QUERY_HIT, i),
|
|
64
|
+
layer="knowledge")
|
|
65
|
+
cg.flush()
|
|
66
|
+
return cg
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def count_score(cg, fn):
|
|
70
|
+
"""包装实例 _score:记录每笔 (docs 数, scored);另存 docs 引用于旁表。"""
|
|
71
|
+
calls = [] # [docs_n]
|
|
72
|
+
pairs = [] # [(docs, scored)]
|
|
73
|
+
real = MdCGOS._score
|
|
74
|
+
|
|
75
|
+
def _spy(self, docs, *a, **kw):
|
|
76
|
+
out = real(self, docs, *a, **kw)
|
|
77
|
+
calls.append(len(docs))
|
|
78
|
+
# 记录时拷贝:_emit 会对 scored 列表原地排序(记录引用会被事后变更)
|
|
79
|
+
pairs.append((list(docs), list(out)))
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
cg._score = _spy.__get__(cg, MdCGOS)
|
|
83
|
+
try:
|
|
84
|
+
return fn(), calls, pairs
|
|
85
|
+
finally:
|
|
86
|
+
del cg._score # 还原类属性查找
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
91
|
+
root = tempfile.mkdtemp(prefix="mdcg_score_once_")
|
|
92
|
+
cg = None
|
|
93
|
+
try:
|
|
94
|
+
cg = build_lib(root)
|
|
95
|
+
cg.search(QUERY_MISS) # 预热(读缓存装载,非度量面)
|
|
96
|
+
|
|
97
|
+
print("== T3 无交集查询:打分恰一次/文档 ==")
|
|
98
|
+
for rep in range(2):
|
|
99
|
+
(res, meta), calls, _ = count_score(
|
|
100
|
+
cg, lambda: cg.search(QUERY_MISS))
|
|
101
|
+
check("T3a rep%d _score 调用数 == 2(旧 4)" % rep,
|
|
102
|
+
len(calls) == 2, f"calls={calls}")
|
|
103
|
+
check("T3b rep%d per-call docs == [0, 600](旧 [0,0,600,500])" % rep,
|
|
104
|
+
calls == [0, 600], f"calls={calls}")
|
|
105
|
+
check("T3c rep%d 共打分 600 文档(旧 1100,省 45)" % rep,
|
|
106
|
+
sum(calls) == 600, f"total={sum(calls)}")
|
|
107
|
+
check("T3d rep%d tier/scanned/结果规模不变" % rep,
|
|
108
|
+
meta.get("tier") == "T3_global_scan"
|
|
109
|
+
and meta.get("scanned") == N and len(res) == 20,
|
|
110
|
+
f"tier={meta.get('tier')} scanned={meta.get('scanned')} "
|
|
111
|
+
f"n={len(res)}")
|
|
112
|
+
|
|
113
|
+
print("== T2 有交集查询:打分恰一次/文档 ==")
|
|
114
|
+
for rep in range(2):
|
|
115
|
+
(res, meta), calls, _ = count_score(
|
|
116
|
+
cg, lambda: cg.search(QUERY_HIT))
|
|
117
|
+
check("T2a rep%d _score 调用数 == 1(旧 2)" % rep,
|
|
118
|
+
len(calls) == 1, f"calls={calls}")
|
|
119
|
+
check("T2b rep%d 共打分 600 文档(旧 1100)" % rep,
|
|
120
|
+
calls == [600], f"calls={calls}")
|
|
121
|
+
check("T2c rep%d tier 不变(T2_global_like)" % rep,
|
|
122
|
+
meta.get("tier") == "T2_global_like",
|
|
123
|
+
f"tier={meta.get('tier')}")
|
|
124
|
+
|
|
125
|
+
print("== 纯度:透传 scored ≡ 现算 _score(不改结果的直接证据)==")
|
|
126
|
+
(res, meta), calls, pairs = count_score(
|
|
127
|
+
cg, lambda: cg.search(QUERY_MISS))
|
|
128
|
+
# 与 search 同口径重建 q/qb(mdcos.py:751-773 的打分输入)
|
|
129
|
+
from md_cg.mdcg import bigrams, normalize_en, en_zh_bigrams
|
|
130
|
+
from md_cg.semantic.unify import unify_query
|
|
131
|
+
q_p = unify_query(QUERY_MISS)
|
|
132
|
+
qb_p = bigrams(normalize_en(q_p)) | en_zh_bigrams(q_p)
|
|
133
|
+
pure = True
|
|
134
|
+
for i, (docs, scored) in enumerate(pairs):
|
|
135
|
+
again = MdCGOS._score(cg, docs, q_p, qb_p, None)
|
|
136
|
+
if again != scored:
|
|
137
|
+
pure = False
|
|
138
|
+
print(f" 第{i}笔复算不一致: {len(docs)} docs")
|
|
139
|
+
check("P1a 每笔 (docs→scored) 复算逐位相等(_score 纯函数)", pure)
|
|
140
|
+
|
|
141
|
+
print("== 确定性:同查询两轮结果逐位一致 ==")
|
|
142
|
+
r1, m1 = cg.search(QUERY_MISS)
|
|
143
|
+
r2, m2 = cg.search(QUERY_MISS)
|
|
144
|
+
ids1 = [(x[0].get("id"), round(x[1], 12)) for x in r1]
|
|
145
|
+
ids2 = [(x[0].get("id"), round(x[1], 12)) for x in r2]
|
|
146
|
+
check("D1a 两轮结果 id 序与分值逐位一致", ids1 == ids2,
|
|
147
|
+
f"n={len(ids1)}/{len(ids2)}")
|
|
148
|
+
rh1, _ = cg.search(QUERY_HIT)
|
|
149
|
+
rh2, _ = cg.search(QUERY_HIT)
|
|
150
|
+
check("D1b T2 路径两轮结果逐位一致",
|
|
151
|
+
[(x[0].get("id"), round(x[1], 12)) for x in rh1]
|
|
152
|
+
== [(x[0].get("id"), round(x[1], 12)) for x in rh2])
|
|
153
|
+
|
|
154
|
+
print("== reach 腿(MDCG_REACH=1):P2-4 模板形态修复 ==")
|
|
155
|
+
# 相邻缺陷(本轮 stash 取证):基类 reach 分支旧 _smap 模板对 scored
|
|
156
|
+
# 的 card dict 调 pooling.doc_key(形参须 (entry,fm,content) 三元组)
|
|
157
|
+
# → MDCG_REACH=1 + 命中集非空即 KeyError: 1(HEAD 原生在位)。
|
|
158
|
+
from md_cg.mdcg import MdCG
|
|
159
|
+
os.environ["MDCG_REACH"] = "1"
|
|
160
|
+
try:
|
|
161
|
+
root2 = tempfile.mkdtemp(prefix="mdcg_score_reach_")
|
|
162
|
+
cg2 = MdCGOS(root2)
|
|
163
|
+
for i in range(30):
|
|
164
|
+
cg2.add("rm_%03d" % i,
|
|
165
|
+
"# 功能名:reach 样本\n# 正文:%s 节点 %d" % (QUERY_HIT, i),
|
|
166
|
+
layer="knowledge")
|
|
167
|
+
cg2.flush()
|
|
168
|
+
try:
|
|
169
|
+
(res_r, meta_r), calls_r, _ = count_score(
|
|
170
|
+
cg2, lambda: cg2.search(QUERY_HIT))
|
|
171
|
+
check("R1a 生产 reach 腿不再 KeyError(HEAD 实测崩)且有结果",
|
|
172
|
+
len(res_r) >= 1, f"n={len(res_r)} tier={meta_r.get('tier')}")
|
|
173
|
+
check("R1b 生产 reach 腿打分 ≤ 2 调(透传无重打)",
|
|
174
|
+
len(calls_r) <= 2, f"calls={calls_r}")
|
|
175
|
+
finally:
|
|
176
|
+
cg2.close()
|
|
177
|
+
shutil.rmtree(root2, ignore_errors=True)
|
|
178
|
+
root3 = tempfile.mkdtemp(prefix="mdcg_score_reachb_")
|
|
179
|
+
cg3 = MdCG(root3)
|
|
180
|
+
for i in range(30):
|
|
181
|
+
cg3.add("bm_%03d" % i,
|
|
182
|
+
"# 功能名:reach 样本\n# 正文:%s 节点 %d" % (QUERY_HIT, i),
|
|
183
|
+
layer="knowledge")
|
|
184
|
+
cg3.flush()
|
|
185
|
+
try:
|
|
186
|
+
res_b, meta_b = cg3.search(QUERY_HIT)
|
|
187
|
+
check("R2a 基类 reach 分支不再 KeyError(stash 取证 HEAD 崩)",
|
|
188
|
+
len(res_b) >= 1, f"n={len(res_b)} tier={meta_b.get('tier')}")
|
|
189
|
+
finally:
|
|
190
|
+
cg3.close()
|
|
191
|
+
shutil.rmtree(root3, ignore_errors=True)
|
|
192
|
+
finally:
|
|
193
|
+
os.environ.pop("MDCG_REACH", None)
|
|
194
|
+
cg.close()
|
|
195
|
+
finally:
|
|
196
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
197
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
198
|
+
|
|
199
|
+
print("\n" + "=" * 60)
|
|
200
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
201
|
+
if FAILS:
|
|
202
|
+
for f in FAILS:
|
|
203
|
+
print(f" - {f}")
|
|
204
|
+
return 0 if FAIL == 0 else 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
sys.exit(main())
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""review 治理面单次调用复用一次解析守卫(issue:同一 jsonl 单调用内重复全量读)。
|
|
4
|
+
|
|
5
|
+
缺陷形态(缺陷报告实测 + 当前代码复核):review_stats 链路每次全量读
|
|
6
|
+
decisions 1 遍(by_decision 统计)+ review_list 全量读 inbox 1 遍;
|
|
7
|
+
review_decide 线性扫 inbox 找 pid + _cascade_dedup 再全量读 inbox 一遍;
|
|
8
|
+
verify_review_record 无 pid 索引线性扫全量 decisions 找单条——解析量随
|
|
9
|
+
M/D 线性放大(M=D=4000 实测 review_stats 15.97ms / decide 21.31ms /
|
|
10
|
+
verify 28.92ms)。
|
|
11
|
+
|
|
12
|
+
本守卫断言(调用计数/对账形态,非时间阈值):
|
|
13
|
+
R1 稳态 review_stats 零全量读:预热装载后 read_jsonl 调用 == 0
|
|
14
|
+
(旧代码每次 decisions+inbox 各 1 次 → 红);
|
|
15
|
+
R2 稳态 review_decide(reject) 零全量读:同上(旧代码 inbox 2 次 → 红);
|
|
16
|
+
R3 稳态 verify_review_record 零全量读:同上(旧代码 decisions 1 次 → 红);
|
|
17
|
+
R4 增量不漏账:外部(他进程形态)append 新裁决/新提案后,统计口径与
|
|
18
|
+
队列视图立即可见;
|
|
19
|
+
R5 语义对账:review_stats 的 records/by_decision/pending/closed 与
|
|
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
|
+
import md_cg.mdcos as mdcos_mod # noqa: E402
|
|
30
|
+
from md_cg.fsutil import append_jsonl, read_jsonl as fs_read_jsonl # noqa: E402
|
|
31
|
+
from md_cg.mdcos import MdCGOS, _sig # 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 count_full_reads(fn):
|
|
50
|
+
n = 0
|
|
51
|
+
real = mdcos_mod.read_jsonl
|
|
52
|
+
|
|
53
|
+
def _spy(path, *a, **kw):
|
|
54
|
+
nonlocal n
|
|
55
|
+
n += 1
|
|
56
|
+
return real(path, *a, **kw)
|
|
57
|
+
|
|
58
|
+
mdcos_mod.read_jsonl = _spy
|
|
59
|
+
try:
|
|
60
|
+
return fn(), n
|
|
61
|
+
finally:
|
|
62
|
+
mdcos_mod.read_jsonl = real
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build(root, m=8, d=5):
|
|
66
|
+
cg = MdCGOS(root)
|
|
67
|
+
for i in range(m):
|
|
68
|
+
c = "存量提案 %d 蜂群调度 %d" % (i, i)
|
|
69
|
+
append_jsonl(cg.inbox_log,
|
|
70
|
+
{"t": 1.0, "pid": "prop_g%03d" % i, "id": "n%03d" % i,
|
|
71
|
+
"content": c, "layer": "knowledge", "tags": [],
|
|
72
|
+
"condition_space": {}, "verify": {},
|
|
73
|
+
"payload_hash": _sig(c), "actor": "g"})
|
|
74
|
+
for j in range(d):
|
|
75
|
+
append_jsonl(cg.decisions_log,
|
|
76
|
+
{"t": 1.0, "pid": "prop_g%03d" % j, "decision": "accept",
|
|
77
|
+
"status": "accepted", "round": 1, "actor": "g"})
|
|
78
|
+
return cg
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
root = tempfile.mkdtemp(prefix="mdcg_review_guard_")
|
|
83
|
+
try:
|
|
84
|
+
cg = build(root)
|
|
85
|
+
cg.review_stats() # 预热装载(不计数)
|
|
86
|
+
|
|
87
|
+
# R1 稳态 review_stats 零全量读
|
|
88
|
+
print("== R1 稳态 review_stats 零全量读 ==")
|
|
89
|
+
st1, n1 = count_full_reads(lambda: cg.review_stats())
|
|
90
|
+
check("R1a 预热后 review_stats 全量 read_jsonl==0(旧每次 2 次)",
|
|
91
|
+
n1 == 0, f"full_reads={n1}")
|
|
92
|
+
check("R1b 统计口径 sane(records≥d、pending≥1)",
|
|
93
|
+
st1["records"] >= 5 and st1["pending"] >= 1, str(st1)[:100])
|
|
94
|
+
|
|
95
|
+
# R2 稳态 review_decide(reject) 零全量读
|
|
96
|
+
print("== R2 稳态 review_decide(reject) 零全量读 ==")
|
|
97
|
+
def _decide():
|
|
98
|
+
_decide.i += 1
|
|
99
|
+
pid = cg.propose("c1", "纯 pending 候选 星云编译 %d" % _decide.i)
|
|
100
|
+
return cg.review_decide(pid, "reject", reason="g")
|
|
101
|
+
_decide.i = 0
|
|
102
|
+
_decide() # 预热:装载 phash 索引 +
|
|
103
|
+
out2, n2 = count_full_reads(_decide) # decisions 创建后的 pid 状态缓存
|
|
104
|
+
check("R2a decide(reject) 全量 read_jsonl==0(旧 inbox 2 次;"
|
|
105
|
+
"首装装载属预期不在此窗)",
|
|
106
|
+
n2 == 0, f"full_reads={n2}")
|
|
107
|
+
check("R2b 裁决 ok", out2.get("ok"), str(out2)[:80])
|
|
108
|
+
|
|
109
|
+
# R3 稳态 verify_review_record 零全量读
|
|
110
|
+
print("== R3 稳态 verify_review_record 零全量读 ==")
|
|
111
|
+
def _verify():
|
|
112
|
+
_verify.i += 1
|
|
113
|
+
pid = cg.propose("c2", "复核候选 信任聚合 %d" % _verify.i)
|
|
114
|
+
out = cg.review_decide(pid, "accept", reason="g")
|
|
115
|
+
assert out.get("ok"), out
|
|
116
|
+
return cg.verify_review_record(out["record_node_id"])
|
|
117
|
+
_verify.i = 0
|
|
118
|
+
_verify() # 预热同上
|
|
119
|
+
ok3, n3 = count_full_reads(_verify)
|
|
120
|
+
check("R3a verify 链路全量 read_jsonl==0(旧 decisions 1 次扫全量找单条)",
|
|
121
|
+
n3 == 0, f"full_reads={n3}")
|
|
122
|
+
check("R3b 复核通过", ok3.get("ok") is True, str(ok3)[:80])
|
|
123
|
+
|
|
124
|
+
# R4 增量不漏账:他进程形态 append
|
|
125
|
+
print("== R4 跨进程 append 增量并入 ==")
|
|
126
|
+
append_jsonl(cg.inbox_log,
|
|
127
|
+
{"t": 2.0, "pid": "prop_ext_x", "id": "nx",
|
|
128
|
+
"content": "外部新提案 水位信箱", "layer": "knowledge",
|
|
129
|
+
"tags": [], "condition_space": {}, "verify": {},
|
|
130
|
+
"payload_hash": _sig("外部新提案 水位信箱"),
|
|
131
|
+
"actor": "ext"})
|
|
132
|
+
append_jsonl(cg.decisions_log,
|
|
133
|
+
{"t": 2.0, "pid": "prop_ext_x", "decision": "noop",
|
|
134
|
+
"status": "noop", "round": 1, "actor": "ext"})
|
|
135
|
+
st4 = cg.review_stats()
|
|
136
|
+
check("R4a 外部新裁决进统计(by_decision.noop=1)",
|
|
137
|
+
st4["by_decision"].get("noop") == 1, str(st4["by_decision"]))
|
|
138
|
+
pend_ids = [r.get("pid") for r in cg.review_list()]
|
|
139
|
+
check("R4b 外部新裁决立即可见(prop_ext_x 已关闭,不在 pending)",
|
|
140
|
+
"prop_ext_x" not in pend_ids, f"pending={pend_ids}")
|
|
141
|
+
|
|
142
|
+
# R5 语义对账:与独立全量直读口径逐项一致
|
|
143
|
+
print("== R5 统计口径独立对账 ==")
|
|
144
|
+
by = {}
|
|
145
|
+
for r in fs_read_jsonl(cg.decisions_log):
|
|
146
|
+
d_ = str(r.get("decision") or "").strip() or "unknown"
|
|
147
|
+
by[d_] = by.get(d_, 0) + 1
|
|
148
|
+
n_pid = len({r.get("pid") for r in fs_read_jsonl(cg.decisions_log)
|
|
149
|
+
if r.get("pid")})
|
|
150
|
+
st5 = cg.review_stats()
|
|
151
|
+
check("R5a records/by_decision 与独立直读一致",
|
|
152
|
+
st5["records"] == sum(by.values())
|
|
153
|
+
and st5["by_decision"] == by,
|
|
154
|
+
f"got={st5['by_decision']} want={by}")
|
|
155
|
+
check("R5b proposals 与独立直读 pid 数一致",
|
|
156
|
+
st5["proposals"] == n_pid,
|
|
157
|
+
f"got={st5['proposals']} want={n_pid}")
|
|
158
|
+
finally:
|
|
159
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
160
|
+
|
|
161
|
+
print("\n" + "=" * 60)
|
|
162
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
163
|
+
if FAILS:
|
|
164
|
+
for f in FAILS:
|
|
165
|
+
print(f" - {f}")
|
|
166
|
+
return 0 if FAIL == 0 else 1
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
sys.exit(main())
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""search_rrf 图路种子读缓存守卫(v9:92 / v13 留档复测·修复,第 14 轮)。
|
|
4
|
+
|
|
5
|
+
缺陷形态(缺陷报告实测,40 带 tags/edges 节点临时库,逐位复现):
|
|
6
|
+
_path_graph 种子腿 mdcos.py `node = self.get(n["id"])` 在边循环前执行——
|
|
7
|
+
get 裸 open+nodefile.loads(mdcg.py get 不在 readcache 包装面,install 只包
|
|
8
|
+
cg._read),search_rrf 默认 paths 含 graph → 每查询固定 ≤5 次盘读
|
|
9
|
+
(Q1 loads=45=词法 40+种子 5;Q2 增量恰 5、get 恒 5/查;graph 零产出也
|
|
10
|
+
在边循环前发生)。边目标(:1017)早已走 self._read 同形。
|
|
11
|
+
|
|
12
|
+
修复口径:种子读改走 self._read(by_id[...])(读缓存覆盖,Q2 增量归零);
|
|
13
|
+
种子 fm.id≠文件名等 by_id 不含形态回落 get 保旧口径(冷路径,正确性优先)。
|
|
14
|
+
种子来自 lexical(entries/_candidates 派生,MdCGSecure._candidates 已过
|
|
15
|
+
_readable)——绕过 get 的读隔离门无损失;图路扩展产物本就由
|
|
16
|
+
MdCGSecure.search_rrf 终态二次过滤兜底。
|
|
17
|
+
|
|
18
|
+
本守卫(调用计数断言形态,非时间阈值):
|
|
19
|
+
S1 热缓存后同查询 loads 增量 == 0(旧实现恒 5 → 红);
|
|
20
|
+
S2 热缓存后同查询 get 调用 == 0(旧实现恒 5 → 红);
|
|
21
|
+
S3 图路功能在位:词法零命中的「幽灵」节点经种子边一跳进入结果
|
|
22
|
+
(graph 路不是被静默关掉);
|
|
23
|
+
S4 两轮查询结果 ids 逐位一致(确定性);meta.paths 含四路计数;
|
|
24
|
+
S5 开/关读缓存(MDCG_READ_CACHE=1 vs 0)同库同查询 ids 逐位一致
|
|
25
|
+
(缓存只改速度不改结果)。
|
|
26
|
+
|
|
27
|
+
运行:python -m md_cg.test_rrf_graph_seed_cache
|
|
28
|
+
"""
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
|
|
34
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
35
|
+
|
|
36
|
+
from md_cg import nodefile # noqa: E402
|
|
37
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
38
|
+
|
|
39
|
+
PASS = 0
|
|
40
|
+
FAIL = 0
|
|
41
|
+
FAILS = []
|
|
42
|
+
|
|
43
|
+
QUERY = "蜂群调度"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check(name, cond, detail=""):
|
|
47
|
+
global PASS, FAIL
|
|
48
|
+
if cond:
|
|
49
|
+
PASS += 1
|
|
50
|
+
print(f" OK {name}")
|
|
51
|
+
else:
|
|
52
|
+
FAIL += 1
|
|
53
|
+
FAILS.append(name)
|
|
54
|
+
print(f" FAIL {name} {detail}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_lib(root, n=40):
|
|
58
|
+
cg = MdCGOS(root)
|
|
59
|
+
for i in range(n):
|
|
60
|
+
edges = []
|
|
61
|
+
if i % 4 == 0: # 部分节点带边(指向池内目标)
|
|
62
|
+
edges = [{"target": "mem_%03d" % ((i + 1) % n),
|
|
63
|
+
"relation_type": "related_to"}]
|
|
64
|
+
if i == 0:
|
|
65
|
+
# 幽灵目标:词法零命中(正文无查询词),只经种子边一跳可达
|
|
66
|
+
edges.append({"target": "ghost_x", "relation_type": "related_to"})
|
|
67
|
+
cg.add("mem_%03d" % i,
|
|
68
|
+
"# 功能名:图路样本 %d\n# 正文:%s 节点 %d" % (i, QUERY, i),
|
|
69
|
+
layer="knowledge",
|
|
70
|
+
tags=["tag_%d" % (i % 5)] if i % 3 == 0 else None,
|
|
71
|
+
edges=edges)
|
|
72
|
+
cg.add("ghost_x", "# 功能名:幽灵目标\n# 正文:完全不相关的孤岛内容",
|
|
73
|
+
layer="knowledge")
|
|
74
|
+
cg.flush()
|
|
75
|
+
return cg
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def counters(cg, fn):
|
|
79
|
+
"""同时计数 nodefile.loads 与实例 get。"""
|
|
80
|
+
st = {"loads": 0, "gets": 0}
|
|
81
|
+
real_loads = nodefile.loads
|
|
82
|
+
real_get = MdCGOS.get
|
|
83
|
+
|
|
84
|
+
def _spy_loads(*a, **kw):
|
|
85
|
+
st["loads"] += 1
|
|
86
|
+
return real_loads(*a, **kw)
|
|
87
|
+
|
|
88
|
+
def _spy_get(self, node_id):
|
|
89
|
+
st["gets"] += 1
|
|
90
|
+
return real_get(self, node_id)
|
|
91
|
+
|
|
92
|
+
nodefile.loads = _spy_loads
|
|
93
|
+
cg.get = _spy_get.__get__(cg, MdCGOS)
|
|
94
|
+
try:
|
|
95
|
+
return fn(), st
|
|
96
|
+
finally:
|
|
97
|
+
nodefile.loads = real_loads
|
|
98
|
+
del cg.get
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
root = tempfile.mkdtemp(prefix="mdcg_rrf_seed_")
|
|
103
|
+
cg = None
|
|
104
|
+
try:
|
|
105
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
106
|
+
cg = build_lib(root)
|
|
107
|
+
(r1, m1), c1 = counters(cg, lambda: cg.search_rrf(QUERY))
|
|
108
|
+
ids1 = [x[0].get("id") for x in r1]
|
|
109
|
+
check("S0a 首查正常(40+1 节点池,词法+图路融合)",
|
|
110
|
+
len(r1) >= 1 and c1["loads"] > 0,
|
|
111
|
+
f"n={len(r1)} loads={c1['loads']}")
|
|
112
|
+
|
|
113
|
+
(r2, m2), c2 = counters(cg, lambda: cg.search_rrf(QUERY))
|
|
114
|
+
ids2 = [x[0].get("id") for x in r2]
|
|
115
|
+
check("S1a 热缓存后同查询 loads 增量 == 0(旧实现恒 5)",
|
|
116
|
+
c2["loads"] == 0, f"loads={c2['loads']}")
|
|
117
|
+
check("S2a 热缓存后同查询 get 调用 == 0(旧实现恒 5,种子改走 _read)",
|
|
118
|
+
c2["gets"] == 0, f"gets={c2['gets']}")
|
|
119
|
+
check("S3a 图路功能在位:词法零命中的 ghost 经种子边一跳进入结果",
|
|
120
|
+
"ghost_x" in ids1, f"ghost in={('ghost_x' in ids1)}")
|
|
121
|
+
paths_meta = (m2.get("paths") or {})
|
|
122
|
+
check("S3b meta.paths 四路计数在位(graph 未被静默关掉)",
|
|
123
|
+
{"lexical", "graph"} <= set(paths_meta),
|
|
124
|
+
f"paths={paths_meta}")
|
|
125
|
+
check("S4a 两轮查询结果 ids 逐位一致", ids1 == ids2,
|
|
126
|
+
f"n={len(ids1)}/{len(ids2)}")
|
|
127
|
+
|
|
128
|
+
# S5 开/关读缓存结果一致(新实例同库同查询)
|
|
129
|
+
r_on = ids2
|
|
130
|
+
os.environ["MDCG_READ_CACHE"] = "0"
|
|
131
|
+
try:
|
|
132
|
+
cg0 = MdCGOS(root)
|
|
133
|
+
r_off, _ = cg0.search_rrf(QUERY)
|
|
134
|
+
cg0.close()
|
|
135
|
+
check("S5a 读缓存开/关同库同查询 ids 逐位一致",
|
|
136
|
+
[x[0].get("id") for x in r_off] == r_on,
|
|
137
|
+
f"off={[x[0].get('id') for x in r_off][:5]}")
|
|
138
|
+
finally:
|
|
139
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
140
|
+
cg.close()
|
|
141
|
+
finally:
|
|
142
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
143
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
144
|
+
|
|
145
|
+
print("\n" + "=" * 60)
|
|
146
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
147
|
+
if FAILS:
|
|
148
|
+
for f in FAILS:
|
|
149
|
+
print(f" - {f}")
|
|
150
|
+
return 0 if FAIL == 0 else 1
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
sys.exit(main())
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""安全审计修复守卫(批次 24,外部审查报告 P0-1/P1-1/P2-1/P1-8)。
|
|
4
|
+
|
|
5
|
+
P0-1:node_id 是模型可控输入、直接拼落盘路径——`..` 穿越出 root、
|
|
6
|
+
Windows 绝对路径(os.path.join 丢弃前缀)= 任意 .md 覆盖。修复 = 白名单
|
|
7
|
+
+ realpath 纵深闸。守卫断言(能红修复前实现):
|
|
8
|
+
- 穿越/绝对路径/超长/空 id 必 ValueError;
|
|
9
|
+
- 合法 id(存量形态)照常写入。
|
|
10
|
+
P1-1:mdcg_* 工具此前完全绕过 ops_allow 白名单——只读令牌可经
|
|
11
|
+
mdcg_remember 写入。守卫断言(能红修复前实现):
|
|
12
|
+
- ops_allow=["read"] 的 principal 调 mdcg_remember 必 AccessDenied;
|
|
13
|
+
- ops_allow=None(不限)令牌照常放行(兼容性);
|
|
14
|
+
- 读工具在 read 令牌下放行。
|
|
15
|
+
P1-8:tarfile filter="data"(静态断言 + import 回归)。
|
|
16
|
+
"""
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, 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 main():
|
|
40
|
+
from md_cg.mdcg import _NODE_ID_RE
|
|
41
|
+
from md_cg.mdcos import MdCGSecure
|
|
42
|
+
from md_cg.mcp_server import _dispatch, _MDCG_OP_REQUIRE
|
|
43
|
+
|
|
44
|
+
root = tempfile.mkdtemp(prefix="mdcg_secaudit_")
|
|
45
|
+
# 生产形态(MdCGSecure——mdcg_search 分支走 OS 层签名,基类 MdCG 无
|
|
46
|
+
# roles 参数;P1-1c 因此必须用安全实例而非基类)
|
|
47
|
+
cg = MdCGSecure(root)
|
|
48
|
+
|
|
49
|
+
print("== P0-1 node_id 白名单 + realpath 纵深闸 ==")
|
|
50
|
+
bad_ids = [
|
|
51
|
+
(".. 穿越", "../evil"),
|
|
52
|
+
("多层穿越", "sub/../../../evil"),
|
|
53
|
+
("Windows 绝对路径", "C:/Users/test/evil"),
|
|
54
|
+
("类 Unix 绝对路径", "/etc/evil"),
|
|
55
|
+
("隐式 ..", "a/../b..c"),
|
|
56
|
+
("空 id", ""),
|
|
57
|
+
("超长 id", "a" * 129),
|
|
58
|
+
]
|
|
59
|
+
for label, nid in bad_ids:
|
|
60
|
+
try:
|
|
61
|
+
cg.add(nid, "# 功能名:x\n# 正文:y", layer="knowledge")
|
|
62
|
+
check(f"P0-1 拒绝 {label}", False, "未抛异常且写入成功")
|
|
63
|
+
except ValueError:
|
|
64
|
+
check(f"P0-1 拒绝 {label}", True)
|
|
65
|
+
except Exception as e: # noqa: BLE001
|
|
66
|
+
check(f"P0-1 拒绝 {label}", False, f"异常类型错误: {type(e).__name__}: {e}")
|
|
67
|
+
|
|
68
|
+
ok_ids = ["mem_1790156428627", "n00001", "mde_abcdef123456",
|
|
69
|
+
"uuid-4f2b-9c.a-d", "a" * 128]
|
|
70
|
+
ok = True
|
|
71
|
+
for i, nid in enumerate(ok_ids):
|
|
72
|
+
try:
|
|
73
|
+
cg.add(nid, f"# 功能名:合法 {i}\n# 正文:白名单内存活样本 {i}",
|
|
74
|
+
layer="knowledge")
|
|
75
|
+
except Exception as e: # noqa: BLE001
|
|
76
|
+
ok = False
|
|
77
|
+
check(f"P0-1 合法 id {nid[:16]}… 照常写入", False,
|
|
78
|
+
f"{type(e).__name__}: {e}")
|
|
79
|
+
check("P0-1 合法 id(存量 5 形态)照常写入", ok and _NODE_ID_RE.match("mem_1"))
|
|
80
|
+
|
|
81
|
+
print("== P1-1 mdcg_* op 闸(能红无闸实现)==")
|
|
82
|
+
from md_cg.security import Principal
|
|
83
|
+
|
|
84
|
+
def principal_with(ops):
|
|
85
|
+
return Principal(actor="auditor", clearance="secret",
|
|
86
|
+
can_write=True, role="recorder", auth_mode="test",
|
|
87
|
+
ops_allow=ops)
|
|
88
|
+
|
|
89
|
+
# 只读令牌:mdcg_remember 必拒(能红:修复前无闸直接写入)
|
|
90
|
+
cg.principal = principal_with(["read"])
|
|
91
|
+
try:
|
|
92
|
+
_dispatch(cg, "mdcg_remember", {"content": "# 功能名:越权\n# 正文:x",
|
|
93
|
+
"layer": "contextual"})
|
|
94
|
+
check("P1-1a 只读令牌 mdcg_remember 必拒", False, "未抛 AccessDenied")
|
|
95
|
+
except Exception as e: # noqa: BLE001
|
|
96
|
+
check("P1-1a 只读令牌 mdcg_remember 必拒(AccessDenied)",
|
|
97
|
+
type(e).__name__ == "AccessDenied",
|
|
98
|
+
f"{type(e).__name__}: {e}")
|
|
99
|
+
|
|
100
|
+
# ops_allow=None(不限)令牌:放行(兼容性——存量直构形态零影响;
|
|
101
|
+
# _in_scope(None)=True 是「未声明=不限制」的既有语义)
|
|
102
|
+
saved = cg.principal
|
|
103
|
+
cg.principal = principal_with(None)
|
|
104
|
+
try:
|
|
105
|
+
res = _dispatch(cg, "mdcg_remember",
|
|
106
|
+
{"content": "# 功能名:兼容\n# 正文:不限令牌照常",
|
|
107
|
+
"layer": "contextual"})
|
|
108
|
+
check("P1-1b ops_allow=None(不限)令牌照常放行", True)
|
|
109
|
+
except Exception as e: # noqa: BLE001
|
|
110
|
+
check("P1-1b ops_allow=None(不限)令牌照常放行", False,
|
|
111
|
+
f"{type(e).__name__}: {e}")
|
|
112
|
+
finally:
|
|
113
|
+
cg.principal = saved
|
|
114
|
+
|
|
115
|
+
# 读令牌:读工具放行
|
|
116
|
+
cg.principal = principal_with(["read"])
|
|
117
|
+
try:
|
|
118
|
+
res = _dispatch(cg, "mdcg_search", {"query": "星云"})
|
|
119
|
+
check("P1-1c 读令牌读工具放行", not isinstance(res, dict) or not res.get("error"),
|
|
120
|
+
str(res)[:120])
|
|
121
|
+
except Exception as e: # noqa: BLE001
|
|
122
|
+
check("P1-1c 读令牌读工具放行", False, f"{type(e).__name__}: {e}")
|
|
123
|
+
cg.principal = None
|
|
124
|
+
|
|
125
|
+
# 映射完备性:mdcg_* 全部注册工具都被映射覆盖(防未来新增工具漏闸)
|
|
126
|
+
from md_cg import mcp_server as ms
|
|
127
|
+
import re as _re
|
|
128
|
+
src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
129
|
+
"mcp_server.py"), encoding="utf-8").read()
|
|
130
|
+
registered = set(_re.findall(r'"name": "(mdcg_[a-z_]+)"', src))
|
|
131
|
+
uncovered = sorted(registered - set(_MDCG_OP_REQUIRE) - {"mdcg_service_info"})
|
|
132
|
+
check("P1-1d mdcg_* 注册工具全覆盖(除自描述 service_info)",
|
|
133
|
+
not uncovered, f"uncovered={uncovered}")
|
|
134
|
+
|
|
135
|
+
print("== P1-8 tarfile filter ==")
|
|
136
|
+
src_cf = open(os.path.join(os.path.dirname(os.path.dirname(
|
|
137
|
+
os.path.abspath(__file__))), "scripts", "criteria_fingerprint.py"),
|
|
138
|
+
encoding="utf-8").read()
|
|
139
|
+
check("P1-8 extractall 显式 filter=\"data\"", 'extractall(tmp, filter="data")'
|
|
140
|
+
in src_cf)
|
|
141
|
+
|
|
142
|
+
# 清理
|
|
143
|
+
import shutil
|
|
144
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
145
|
+
|
|
146
|
+
print("\n" + "=" * 60)
|
|
147
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
148
|
+
if FAILS:
|
|
149
|
+
for f in FAILS:
|
|
150
|
+
print(f" - {f}")
|
|
151
|
+
return 0 if FAIL == 0 else 1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
sys.exit(main())
|