@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,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())
|