@furongjun1999/dsh-memory 0.5.1 → 0.6.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 +588 -552
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
- package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
- package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- 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_v18.md +183 -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_v19.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_v20.md +283 -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_v21.md +224 -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_v22.md +223 -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_v23.md +293 -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_v24.md +303 -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_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +20 -0
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -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/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.js +24 -2
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.d.ts +88 -0
- package/lib/init.js +302 -0
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evidence.py +585 -582
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/fsutil.py +454 -8
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4152 -3818
- package/md_cg/mdcg.py +4208 -3462
- package/md_cg/mdcos.py +4491 -4137
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/review_cli.py +49 -4
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1299 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +166 -0
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +516 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +8 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/cli.ts +65 -0
- package/src/hooks.ts +229 -16
- package/src/init.ts +361 -0
- package/src/lib/prompt_safety.ts +88 -1
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""B1/B2 写面守卫:自动节点 id 唯一性 + 密级落盘透传与 fail-closed。
|
|
3
|
+
|
|
4
|
+
对应缺陷:
|
|
5
|
+
B1 自动节点 id 撞毫秒静默覆盖(原实现 `"mem_" + 毫秒` 两处各写一份;
|
|
6
|
+
同毫秒自动写入铸出同一 id → `add` 的 upsert 语义静默顶替前者,
|
|
7
|
+
返回 committed=true、正文全库 0 命中,无任何失败信号);
|
|
8
|
+
B2 密级静默降级(writepipe 落盘实参表缺 sensitivity,而审核闸 payload
|
|
9
|
+
带着它 → 声明 private 的正文以明文 + fm internal 落盘)。
|
|
10
|
+
|
|
11
|
+
断言清单(每条对一个可定点变异的判据):
|
|
12
|
+
B1-a 形态:`mem_<毫秒>_<6位hex>`,同时过 `_NODE_ID_RE` 与 `linkref.ID_SHAPE_RE`
|
|
13
|
+
B1-b 加熵:同毫秒 500 次铸造零重复
|
|
14
|
+
B1-c 存在性闸:撞既有节点即换随机段重生成,既有节点正文未被顶替
|
|
15
|
+
B1-d 有界 fail-closed:连续撞满 attempts 次 → RuntimeError
|
|
16
|
+
B1-e 端到端:同毫秒两笔自动写入 → 两个 id、两笔正文各自可读回
|
|
17
|
+
B1-f 显式 node_id 的「同 id 即改写」upsert 语义原样保留(要求④)
|
|
18
|
+
B1-g 单点:三处调用面零裸毫秒铸造字面量且都委托 `mint_auto_id`;
|
|
19
|
+
全仓 `mint_auto_id` 只有一份定义
|
|
20
|
+
B1-h `linkref_backfill.do_propose` 的提案 id 走同一铸造点
|
|
21
|
+
B2-a writepipe 链尾落盘面透传 sensitivity(private → fm private + 密文)
|
|
22
|
+
B2-b `mdcg_remember` 非 gated 分支透传
|
|
23
|
+
B2-c gated 分支(mcp_server 与 writepipe 两条)透传
|
|
24
|
+
B2-d 密级一致性闸判据(声明≠落盘 / 加密级当明文)与放行面
|
|
25
|
+
B2-e 端到端 fail-closed:闸命中时**未落盘**(盘上无该文件)
|
|
26
|
+
|
|
27
|
+
沙箱(硬约束 1):一切读写都在 tempfile.mkdtemp 内——aux(密钥/身份)、
|
|
28
|
+
认知图根、主密钥全部指到沙箱,**绝不碰在役库**。
|
|
29
|
+
"""
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import shutil
|
|
34
|
+
import tempfile
|
|
35
|
+
|
|
36
|
+
# 沙箱必须在**任何** md_cg 子模块 import 之前设好:`crypto.MASTER_FILE` 在
|
|
37
|
+
# 模块导入时求值(`aux_root()`),晚设会让密钥面指回 ~/.mcg。
|
|
38
|
+
_SANDBOX = tempfile.mkdtemp(prefix="b1b2_sandbox_")
|
|
39
|
+
os.environ["MDCG_AUX_ROOT"] = _SANDBOX
|
|
40
|
+
os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
|
|
41
|
+
os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
|
|
42
|
+
os.environ.pop("MDCG_POLICY_FILE", None)
|
|
43
|
+
|
|
44
|
+
from . import crypto, linkref, mdcg, mcp_server, nodefile, writepipe # noqa: E402
|
|
45
|
+
from .mdcos import MdCGSecure # noqa: E402
|
|
46
|
+
from .security import Principal # noqa: E402
|
|
47
|
+
|
|
48
|
+
_KEK = os.urandom(32)
|
|
49
|
+
_OK = 0
|
|
50
|
+
_BAD = []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check(name, cond, detail=""):
|
|
54
|
+
global _OK
|
|
55
|
+
if cond:
|
|
56
|
+
_OK += 1
|
|
57
|
+
print(" ok %s" % name)
|
|
58
|
+
else:
|
|
59
|
+
_BAD.append(name)
|
|
60
|
+
print(" FAIL %s %s" % (name, detail))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _mk_cg(tag, clearance="secret", role="designer"):
|
|
64
|
+
root = os.path.join(_SANDBOX, "lib_" + tag)
|
|
65
|
+
p = Principal(tenant="t1", actor="alice", role=role, clearance=clearance,
|
|
66
|
+
can_write=True, can_admin=True)
|
|
67
|
+
return MdCGSecure(root, principal=p, master_key=_KEK)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _policy(mark):
|
|
71
|
+
path = os.path.join(_SANDBOX, "policy_%s.json" % mark)
|
|
72
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
73
|
+
json.dump({"forbidden": [], "required": [mark]}, f)
|
|
74
|
+
os.environ["MDCG_POLICY_FILE"] = path
|
|
75
|
+
return path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _raw(cg, nid):
|
|
79
|
+
"""读节点盘上原文(不经解密),返回 (text, fm, content)。"""
|
|
80
|
+
e = cg.index["nodes"][nid]
|
|
81
|
+
with open(cg._node_disk_path(e), encoding="utf-8") as f:
|
|
82
|
+
text = f.read()
|
|
83
|
+
fm, content = nodefile.loads(text)
|
|
84
|
+
return text, fm, content
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _FakeTime:
|
|
88
|
+
"""固定毫秒的时间替身(只覆写 time(),其余属性委托真实模块)。"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, ts):
|
|
91
|
+
self._ts = ts
|
|
92
|
+
|
|
93
|
+
def time(self):
|
|
94
|
+
return self._ts
|
|
95
|
+
|
|
96
|
+
def __getattr__(self, k):
|
|
97
|
+
import time as _t
|
|
98
|
+
return getattr(_t, k)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
print("\n[B1] 自动 id 唯一性(唯一铸造点)")
|
|
103
|
+
|
|
104
|
+
# ---- B1-a 形态 ----
|
|
105
|
+
nids = [mdcg.mint_auto_id(None) for _ in range(5)]
|
|
106
|
+
_check("B1-a1 形态 mem_<毫秒>_<6位hex>",
|
|
107
|
+
all(re.match(r"^mem_\d+_[0-9a-f]{6}$", n) for n in nids), nids[:2])
|
|
108
|
+
_check("B1-a2 过引擎 id 白名单 _NODE_ID_RE(含无 ..)",
|
|
109
|
+
all(mdcg._NODE_ID_RE.match(n) and ".." not in n
|
|
110
|
+
for n in nids), nids[:2])
|
|
111
|
+
_check("B1-a3 过链引用形态 ID_SHAPE_RE(整串被当一个 id)",
|
|
112
|
+
all(linkref.ID_SHAPE_RE.match(n) for n in nids), nids[:2])
|
|
113
|
+
_check("B1-a4 长度 ≤128",
|
|
114
|
+
all(len(n) <= 128 for n in nids), [len(n) for n in nids])
|
|
115
|
+
|
|
116
|
+
# ---- B1-b 加熵:同毫秒零重复 ----
|
|
117
|
+
# 6 位 hex = 16.7M 值域,500 次采样存在约 0.7% 的**生日碰撞**(理论允许),
|
|
118
|
+
# 故断言留 1 次余量——「无加熵」的变异下 unique 恒为 1,仍必红。
|
|
119
|
+
same_ms = {mdcg.mint_auto_id(None, now_ms=1790743939104)
|
|
120
|
+
for _ in range(500)}
|
|
121
|
+
_check("B1-b1 同毫秒 500 次铸造几乎零重复", len(same_ms) >= 499,
|
|
122
|
+
"unique=%d" % len(same_ms))
|
|
123
|
+
|
|
124
|
+
# ---- B1-c 存在性闸:撞车换随机段,既有节点不被顶替 ----
|
|
125
|
+
cg = _mk_cg("b1c")
|
|
126
|
+
ms = 1790743939104
|
|
127
|
+
taken = "mem_%d_111111" % ms
|
|
128
|
+
cg.add(taken, "既有节点正文(不得被顶替)", layer="knowledge")
|
|
129
|
+
seq = iter(["111111", "222222", "333333", "444444", "555555"])
|
|
130
|
+
_orig_hex = mdcg.secrets.token_hex
|
|
131
|
+
try:
|
|
132
|
+
mdcg.secrets.token_hex = lambda n=3: next(seq)
|
|
133
|
+
got = mdcg.mint_auto_id(cg, now_ms=ms)
|
|
134
|
+
finally:
|
|
135
|
+
mdcg.secrets.token_hex = _orig_hex
|
|
136
|
+
_check("B1-c1 撞既有 id → 换随机段重生成(返回第二个)",
|
|
137
|
+
got == "mem_%d_222222" % ms, got)
|
|
138
|
+
got_body = (cg.get(taken) or {}).get("content") or ""
|
|
139
|
+
_check("B1-c2 既有节点正文未被顶替", "既有节点正文" in got_body, got_body[:40])
|
|
140
|
+
|
|
141
|
+
# ---- B1-d 有界 fail-closed ----
|
|
142
|
+
ms_d = 1790743939200
|
|
143
|
+
cg.add("mem_%d_aaaaaa" % ms_d, "占位节点", layer="knowledge")
|
|
144
|
+
_orig_hex = mdcg.secrets.token_hex
|
|
145
|
+
try:
|
|
146
|
+
mdcg.secrets.token_hex = lambda n=3: "aaaaaa"
|
|
147
|
+
try:
|
|
148
|
+
mdcg.mint_auto_id(cg, now_ms=ms_d)
|
|
149
|
+
_check("B1-d1 连续撞满 → RuntimeError(fail-closed)", False,
|
|
150
|
+
"未抛异常")
|
|
151
|
+
except RuntimeError as exc:
|
|
152
|
+
_check("B1-d1 连续撞满 → RuntimeError(fail-closed)",
|
|
153
|
+
"fail-closed" in str(exc) and "绝不静默顶替" in str(exc),
|
|
154
|
+
str(exc)[:80])
|
|
155
|
+
except Exception as exc: # noqa: BLE001
|
|
156
|
+
_check("B1-d1 连续撞满 → RuntimeError(fail-closed)", False,
|
|
157
|
+
"抛了 %r" % exc)
|
|
158
|
+
finally:
|
|
159
|
+
mdcg.secrets.token_hex = _orig_hex
|
|
160
|
+
|
|
161
|
+
# ---- B1-e 端到端:同毫秒两笔自动写入互不顶替 ----
|
|
162
|
+
_policy("B1MARK")
|
|
163
|
+
cg_e = _mk_cg("b1e")
|
|
164
|
+
pipe = writepipe.install_default_gates(writepipe.WritePipeline())
|
|
165
|
+
fixed = _FakeTime(1790743939.104)
|
|
166
|
+
_orig_time = mdcg.time
|
|
167
|
+
o1 = o2 = None
|
|
168
|
+
err_e = None
|
|
169
|
+
try:
|
|
170
|
+
mdcg.time = fixed
|
|
171
|
+
try:
|
|
172
|
+
o1 = pipe.execute(cg_e, {"content_kind": "text",
|
|
173
|
+
"content": "B1MARK 第一笔自动写入"})
|
|
174
|
+
o2 = pipe.execute(cg_e, {"content_kind": "text",
|
|
175
|
+
"content": "B1MARK 第二笔自动写入"})
|
|
176
|
+
except Exception as exc: # noqa: BLE001
|
|
177
|
+
err_e = "%s: %s" % (type(exc).__name__, str(exc)[:60])
|
|
178
|
+
finally:
|
|
179
|
+
mdcg.time = _orig_time
|
|
180
|
+
o1, o2 = o1 or {}, o2 or {}
|
|
181
|
+
_check("B1-e1 两笔自动写入 id 不同",
|
|
182
|
+
o1.get("id") and o2.get("id") and o1["id"] != o2["id"],
|
|
183
|
+
"%r vs %r %s" % (o1.get("id"), o2.get("id"), err_e or ""))
|
|
184
|
+
b1 = (cg_e.get(o1.get("id")) or {}).get("content") or ""
|
|
185
|
+
b2 = (cg_e.get(o2.get("id")) or {}).get("content") or ""
|
|
186
|
+
_check("B1-e2 第一笔正文未被第二笔顶替(两笔都在)",
|
|
187
|
+
"第一笔" in b1 and "第二笔" in b2, (b1[:20], b2[:20]))
|
|
188
|
+
|
|
189
|
+
# ---- B1-f 显式 node_id 的「同 id 即改写」语义原样保留(要求④)----
|
|
190
|
+
o1 = pipe.execute(cg_e, {"node_id": "explicit_upsert_x",
|
|
191
|
+
"content_kind": "text",
|
|
192
|
+
"content": "B1MARK 显式 id 第一次"})
|
|
193
|
+
o2 = pipe.execute(cg_e, {"node_id": "explicit_upsert_x",
|
|
194
|
+
"content_kind": "text",
|
|
195
|
+
"content": "B1MARK 显式 id 第二次改写"})
|
|
196
|
+
body = (cg_e.get("explicit_upsert_x") or {}).get("content") or ""
|
|
197
|
+
_check("B1-f1 显式 id 两次返回同 id", o1.get("id") == o2.get("id")
|
|
198
|
+
== "explicit_upsert_x", "%r %r" % (o1.get("id"), o2.get("id")))
|
|
199
|
+
_check("B1-f2 显式 id 为改写(正文=第二次)",
|
|
200
|
+
"第二次改写" in body and "第一次" not in body, body[:40])
|
|
201
|
+
_check("B1-f3 显式 id 未被换成自动 id(索引键仍在)",
|
|
202
|
+
"explicit_upsert_x" in cg_e.index["nodes"])
|
|
203
|
+
|
|
204
|
+
# ---- B1-g 单点:零裸毫秒铸造字面量 + 一份定义 ----
|
|
205
|
+
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
206
|
+
files = {"md_cg/writepipe.py": os.path.join(repo, "md_cg", "writepipe.py"),
|
|
207
|
+
"md_cg/mcp_server.py": os.path.join(repo, "md_cg", "mcp_server.py"),
|
|
208
|
+
"scripts/linkref_backfill.py": os.path.join(
|
|
209
|
+
repo, "scripts", "linkref_backfill.py")}
|
|
210
|
+
bare_ms = re.compile(r'"mem_" \+ str\(int\(|"mem_%d"')
|
|
211
|
+
bad = []
|
|
212
|
+
for name, path in files.items():
|
|
213
|
+
src = open(path, encoding="utf-8").read()
|
|
214
|
+
if bare_ms.search(src):
|
|
215
|
+
bad.append(name)
|
|
216
|
+
if "mint_auto_id" not in src:
|
|
217
|
+
bad.append(name + "(未委托)")
|
|
218
|
+
_check("B1-g1 三处调用面零裸毫秒铸造且都委托 mint_auto_id", not bad, bad)
|
|
219
|
+
src_mdcg = open(os.path.join(repo, "md_cg", "mdcg.py"),
|
|
220
|
+
encoding="utf-8").read()
|
|
221
|
+
hits = 0
|
|
222
|
+
for name, path in files.items():
|
|
223
|
+
hits += open(path, encoding="utf-8").read().count("def mint_auto_id(")
|
|
224
|
+
hits += src_mdcg.count("def mint_auto_id(")
|
|
225
|
+
_check("B1-g2 mint_auto_id 只有一份定义", hits == 1, "def 出现 %d 次" % hits)
|
|
226
|
+
|
|
227
|
+
# ---- B1-h linkref_backfill 的提案 id 走同一铸造点 ----
|
|
228
|
+
import importlib.util
|
|
229
|
+
lp = os.path.join(repo, "scripts", "linkref_backfill.py")
|
|
230
|
+
spec = importlib.util.spec_from_file_location("lrb_guard", lp)
|
|
231
|
+
lrb = importlib.util.module_from_spec(spec)
|
|
232
|
+
spec.loader.exec_module(lrb)
|
|
233
|
+
cg_h = _mk_cg("b1h")
|
|
234
|
+
seen_h = {}
|
|
235
|
+
orig_pp = cg_h.propose
|
|
236
|
+
|
|
237
|
+
def _spy_propose(node_id, content, **kw):
|
|
238
|
+
seen_h["node_id"] = node_id
|
|
239
|
+
return "pid_guard_probe"
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
cg_h.propose = _spy_propose
|
|
243
|
+
lrb.do_propose(cg_h, [], {"nodes_with_gap": 0, "pairs": 0,
|
|
244
|
+
"nodes_total": 0}, None)
|
|
245
|
+
finally:
|
|
246
|
+
cg_h.propose = orig_pp
|
|
247
|
+
_check("B1-h1 提案 node_id 走唯一铸造点(带熵形态)",
|
|
248
|
+
bool(re.match(r"^mem_\d+_[0-9a-f]{6}$", str(seen_h.get("node_id")))),
|
|
249
|
+
repr(seen_h.get("node_id")))
|
|
250
|
+
|
|
251
|
+
print("\n[B2] 密级落盘透传 + fail-closed 闸")
|
|
252
|
+
|
|
253
|
+
# ---- B2-a writepipe 链尾落盘面透传 ----
|
|
254
|
+
_policy("B2MARK")
|
|
255
|
+
cg_a = _mk_cg("b2a")
|
|
256
|
+
secret_text = "B2MARK 私有正文:银行卡尾号 8888,仅本人可见"
|
|
257
|
+
out = pipe.execute(cg_a, {"content_kind": "text", "content": secret_text,
|
|
258
|
+
"sensitivity": "private"})
|
|
259
|
+
nid_a = out.get("id") or ""
|
|
260
|
+
_check("B2-a0 写入 committed", out.get("committed") is True, repr(out)[:120])
|
|
261
|
+
if nid_a in cg_a.index["nodes"]:
|
|
262
|
+
raw, fm, content = _raw(cg_a, nid_a)
|
|
263
|
+
_check("B2-a1 落盘 fm sensitivity == 声明 private",
|
|
264
|
+
fm.get("sensitivity") == "private", repr(fm.get("sensitivity")))
|
|
265
|
+
_check("B2-a2 落盘正文为密文",
|
|
266
|
+
crypto.is_encrypted(content), str(content)[:40])
|
|
267
|
+
_check("B2-a3 落盘文件不含明文", secret_text not in raw)
|
|
268
|
+
else:
|
|
269
|
+
_check("B2-a1 落盘 fm sensitivity == 声明 private", False, "未落盘")
|
|
270
|
+
|
|
271
|
+
# ---- B2-b mdcg_remember 非 gated 分支 ----
|
|
272
|
+
cg_b = _mk_cg("b2b")
|
|
273
|
+
res = mcp_server._dispatch(cg_b, "mdcg_remember",
|
|
274
|
+
{"content": "B2MARK remember 私有正文",
|
|
275
|
+
"sensitivity": "private",
|
|
276
|
+
"content_kind": "text"})
|
|
277
|
+
nid_b = res.get("id") or ""
|
|
278
|
+
_check("B2-b0 remember 返回 ok", res.get("ok") is True, repr(res)[:120])
|
|
279
|
+
if nid_b in cg_b.index["nodes"]:
|
|
280
|
+
raw, fm, content = _raw(cg_b, nid_b)
|
|
281
|
+
_check("B2-b1 remember 落盘 fm sensitivity == private",
|
|
282
|
+
fm.get("sensitivity") == "private", repr(fm.get("sensitivity")))
|
|
283
|
+
_check("B2-b2 remember 落盘正文为密文",
|
|
284
|
+
crypto.is_encrypted(content), str(content)[:40])
|
|
285
|
+
else:
|
|
286
|
+
_check("B2-b1 remember 落盘 fm sensitivity == private", False,
|
|
287
|
+
"未落盘 id=%r" % nid_b)
|
|
288
|
+
|
|
289
|
+
# ---- B2-c gated 分支透传(mcp_server 与 writepipe 两条)----
|
|
290
|
+
seen = {}
|
|
291
|
+
|
|
292
|
+
def _spy(node_id, content, layer="contextual", **kw):
|
|
293
|
+
seen.update(kw)
|
|
294
|
+
return {"verdict": "DROP", "node_id": node_id, "gate": {}}
|
|
295
|
+
|
|
296
|
+
cg_c = _mk_cg("b2c")
|
|
297
|
+
orig_rg = cg_c.remember_gated
|
|
298
|
+
try:
|
|
299
|
+
cg_c.remember_gated = _spy
|
|
300
|
+
mcp_server._dispatch(cg_c, "mdcg_remember",
|
|
301
|
+
{"content": "B2MARK gated mcp 分支",
|
|
302
|
+
"gated": True, "sensitivity": "private",
|
|
303
|
+
"content_kind": "text"})
|
|
304
|
+
_check("B2-c1 mdcg_remember gated 分支透传 sensitivity",
|
|
305
|
+
seen.get("sensitivity") == "private",
|
|
306
|
+
repr(seen.get("sensitivity")))
|
|
307
|
+
seen.clear()
|
|
308
|
+
pipe.execute(cg_c, {"content": "B2MARK gated writepipe 分支",
|
|
309
|
+
"gated": True, "sensitivity": "private",
|
|
310
|
+
"content_kind": "text", "layer": "contextual"})
|
|
311
|
+
_check("B2-c2 writepipe gated 闸透传 sensitivity",
|
|
312
|
+
seen.get("sensitivity") == "private",
|
|
313
|
+
repr(seen.get("sensitivity")))
|
|
314
|
+
finally:
|
|
315
|
+
cg_c.remember_gated = orig_rg
|
|
316
|
+
|
|
317
|
+
# ---- B2-d 闸判据(声明≠落盘 / 加密级当明文 / 放行面)----
|
|
318
|
+
dek = crypto.provision_dek(_SANDBOX, _KEK, "t1", "alice")
|
|
319
|
+
sealed = crypto.seal_node("payload", dek, "n1", "t1", "alice")
|
|
320
|
+
try:
|
|
321
|
+
mdcg._check_sensitivity_landing("n1", "internal", "private", "明文")
|
|
322
|
+
_check("B2-d1 声明 private 而落 internal → ValueError", False, "未抛")
|
|
323
|
+
except ValueError as exc:
|
|
324
|
+
msg = str(exc)
|
|
325
|
+
_check("B2-d1 声明 private 而落 internal → ValueError",
|
|
326
|
+
"声明值='private'" in msg and "sensitivity='internal'" in msg
|
|
327
|
+
and "未写盘" in msg, msg[:100])
|
|
328
|
+
try:
|
|
329
|
+
mdcg._check_sensitivity_landing("n1", "private", "private", "明文",
|
|
330
|
+
True)
|
|
331
|
+
_check("B2-d2 有密封能力 + 加密级密级却明文 → ValueError", False,
|
|
332
|
+
"未抛")
|
|
333
|
+
except ValueError as exc:
|
|
334
|
+
_check("B2-d2 有密封能力 + 加密级密级却明文 → ValueError",
|
|
335
|
+
"明文落盘" in str(exc), str(exc)[:100])
|
|
336
|
+
try:
|
|
337
|
+
mdcg._check_sensitivity_landing("n1", "secret", None, "明文", True)
|
|
338
|
+
_check("B2-d3 有密封能力 + 未声明但 fm 落 secret + 明文 → ValueError",
|
|
339
|
+
False, "未抛")
|
|
340
|
+
except ValueError:
|
|
341
|
+
_check("B2-d3 有密封能力 + 未声明但 fm 落 secret + 明文 → ValueError",
|
|
342
|
+
True)
|
|
343
|
+
try:
|
|
344
|
+
mdcg._check_sensitivity_landing("n1", "private", "private", sealed,
|
|
345
|
+
True)
|
|
346
|
+
mdcg._check_sensitivity_landing("n1", "internal", None, "明文", True)
|
|
347
|
+
_check("B2-d4 合规面放行(密文 private / 明文 internal)", True)
|
|
348
|
+
except Exception as exc: # noqa: BLE001
|
|
349
|
+
_check("B2-d4 合规面放行(密文 private / 明文 internal)", False,
|
|
350
|
+
repr(exc))
|
|
351
|
+
try:
|
|
352
|
+
mdcg._check_sensitivity_landing("n1", "private", None, "明文", False)
|
|
353
|
+
_check("B2-d5 无密封能力实例(has_sealer=False)→ 放行(既有语义)",
|
|
354
|
+
True)
|
|
355
|
+
except Exception as exc: # noqa: BLE001
|
|
356
|
+
_check("B2-d5 无密封能力实例(has_sealer=False)→ 放行(既有语义)",
|
|
357
|
+
False, repr(exc))
|
|
358
|
+
|
|
359
|
+
# ---- B2-e 端到端 fail-closed 且**未落盘**(密封点被绕过的回归)----
|
|
360
|
+
class _LeakySecure(MdCGSecure):
|
|
361
|
+
"""替身:覆写了密封钩子(=有密封能力)却故意不密封。
|
|
362
|
+
|
|
363
|
+
模拟「_write_node 的密封点被绕过/失效」的回归——闸必须抓住它。
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def _seal_content(self, node_id, content, sensitivity=None):
|
|
367
|
+
return content
|
|
368
|
+
|
|
369
|
+
root_e = os.path.join(_SANDBOX, "lib_b2e")
|
|
370
|
+
leak = _LeakySecure(root_e, principal=Principal(
|
|
371
|
+
tenant="t1", actor="alice", role="designer", clearance="secret",
|
|
372
|
+
can_write=True, can_admin=True), master_key=_KEK)
|
|
373
|
+
try:
|
|
374
|
+
leak.add("leaky_priv_probe", "明文却声明 private", sensitivity="private")
|
|
375
|
+
_check("B2-e1 密封点被绕过的实例 → ValueError", False, "未抛")
|
|
376
|
+
except ValueError as exc:
|
|
377
|
+
_check("B2-e1 密封点被绕过的实例 → ValueError",
|
|
378
|
+
"明文落盘" in str(exc) or "密级" in str(exc), str(exc)[:100])
|
|
379
|
+
_check("B2-e2 闸命中时未落盘(盘上无该文件)",
|
|
380
|
+
not os.path.isfile(os.path.join(root_e, "knowledge", "orphan",
|
|
381
|
+
"leaky_priv_probe.md")))
|
|
382
|
+
|
|
383
|
+
# ---- B2-f 无密封能力的实例落 private 明文 → 放行(既有语义,不得误扩)----
|
|
384
|
+
# 钉住三条既有回归钉死的场景:docindex 私有目录文档 / identity_attribution
|
|
385
|
+
# 的 legacy 无密钥私有条目 / hive_ingest 的 MdCGOS 事件流——它们用基类实例
|
|
386
|
+
# 落 `private` 明文(fm 如实记 private、读面按密级隔离),不是「静默降级」。
|
|
387
|
+
root_f = os.path.join(_SANDBOX, "lib_b2f")
|
|
388
|
+
base = mdcg.MdCG(root_f, autoflush=1)
|
|
389
|
+
err_f = None
|
|
390
|
+
try:
|
|
391
|
+
base.add("legacy_priv_probe", "无密封能力的库落 private 明文",
|
|
392
|
+
layer="knowledge", sensitivity="private")
|
|
393
|
+
except Exception as exc: # noqa: BLE001
|
|
394
|
+
err_f = "%s: %s" % (type(exc).__name__, str(exc)[:80])
|
|
395
|
+
if err_f is None:
|
|
396
|
+
raw_f, fm_f, content_f = _raw(base, "legacy_priv_probe")
|
|
397
|
+
_check("B2-f1 无密封能力实例:private 明文落盘放行(不误伤)",
|
|
398
|
+
fm_f.get("sensitivity") == "private"
|
|
399
|
+
and not crypto.is_encrypted(content_f),
|
|
400
|
+
repr(fm_f.get("sensitivity")))
|
|
401
|
+
else:
|
|
402
|
+
_check("B2-f1 无密封能力实例:private 明文落盘放行(不误伤)", False,
|
|
403
|
+
err_f)
|
|
404
|
+
|
|
405
|
+
print("\n==== B1/B2 写面守卫:%d 通过%s ====" % (
|
|
406
|
+
_OK, ",%d 失败:%s" % (len(_BAD), "; ".join(_BAD)) if _BAD else ""))
|
|
407
|
+
return 1 if _BAD else 0
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
if __name__ == "__main__":
|
|
411
|
+
import sys
|
|
412
|
+
try:
|
|
413
|
+
rc = main()
|
|
414
|
+
finally:
|
|
415
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
416
|
+
os.environ.pop("MDCG_POLICY_FILE", None)
|
|
417
|
+
sys.exit(rc)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# 功能名:B2 密级「声明 → 落盘」透传与 fail-closed 闸守卫(cg(op=write) 端到端)
|
|
3
|
+
# 生效条件:writepipe._executor 的 cg.add 实参表带 sensitivity(:543)、
|
|
4
|
+
# mcp_server.mdcg_remember 两分支带 sensitivity(:3199 / :3213)、
|
|
5
|
+
# 库层 md_cg.mdcg._check_sensitivity_landing(:1079)接线在
|
|
6
|
+
# MdCG._write_node(:2609)时;
|
|
7
|
+
# 沙箱条件:MDCG_ROOT / MDCG_AUX_ROOT / MDCG_MASTER_KEY / MDCG_POLICY_FILE
|
|
8
|
+
# 全部指向 tempfile.mkdtemp 建的独立目录,绝不触在役库。
|
|
9
|
+
# 子功能:① cg(op=write, sensitivity='private') 落盘:frontmatter sensitivity 就是
|
|
10
|
+
# private,且落盘正文不是明文(AEAD 密文、盘上文件不含明文);
|
|
11
|
+
# ② 声明加密级却落盘明文 → fail-closed 拒绝(判据直调 + 写面端到端两路);
|
|
12
|
+
# ③ 同族旁路:mdcg_remember 非 gated 分支的透传(第二处落盘面)。
|
|
13
|
+
# 执行:python -X utf8 -m md_cg.test_b2_sensitivity_landing
|
|
14
|
+
# (cwd = 仓库根;不带参数、无网络、无外部依赖,全程沙箱)
|
|
15
|
+
# 验证方式:本文件自跑(自带断言计数与退出码 rc=0/1);定点变异自证见文件尾
|
|
16
|
+
# `--drop-fix`(抽掉 writepipe._executor 的 sensitivity 透传 → 本守卫必红)。
|
|
17
|
+
# 不适用条件:情感交互|闲聊|纯查询无改动;
|
|
18
|
+
# 无密封能力实例(基类 MdCG / MdCGOS)落 private 明文是**既有语义**
|
|
19
|
+
# (fm 如实记 private、读面按密级隔离),不在本闸判据② 面内——已由
|
|
20
|
+
# B2-B6 显式钉住,防把闸误扩到「无加密能力的库」。
|
|
21
|
+
"""B2 守卫:密级声明在落盘面是否被透传,以及「声明加密级却明文落盘」的 fail-closed。
|
|
22
|
+
|
|
23
|
+
缺陷(修复前):
|
|
24
|
+
writepipe 链尾执行器的 `cg.add` 实参表**缺 sensitivity**,而同文件
|
|
25
|
+
`_gate_audit` 的 payload 带着它交给审核闸——两面口径分叉:审核闸按调用方
|
|
26
|
+
声明的密级判、落盘闸按 DEFAULT_SENSITIVITY 回落 internal,于是声明 private
|
|
27
|
+
的正文以**明文 + fm internal** 落盘(纯漏传,无任何失败信号)。
|
|
28
|
+
|
|
29
|
+
本守卫的判别力锚点(每条断言都可定点变异):
|
|
30
|
+
A 组 cg(op=write, sensitivity='private') 端到端:fm=private + 正文密文 + 盘上无明文。
|
|
31
|
+
B 组 fail-closed:声明 private 而落盘 internal(闸①)/ 加密级却明文(闸②)
|
|
32
|
+
一律 ValueError 且**未落盘**;合规面与「无密封能力实例」放行(防误扩)。
|
|
33
|
+
C 组 mdcg_remember 非 gated 分支(第二处落盘面)同款透传。
|
|
34
|
+
|
|
35
|
+
沙箱(硬约束):一切读写都在 tempfile.mkdtemp 内;跑完 rmtree。绝不碰在役库。
|
|
36
|
+
"""
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
import shutil
|
|
40
|
+
import sys
|
|
41
|
+
import tempfile
|
|
42
|
+
|
|
43
|
+
# 沙箱必须在**任何** md_cg 子模块 import 之前设好(`crypto.MASTER_FILE` 在模块
|
|
44
|
+
# 导入时求值)。
|
|
45
|
+
_SANDBOX = tempfile.mkdtemp(prefix="b2sens_sandbox_")
|
|
46
|
+
os.environ["MDCG_AUX_ROOT"] = _SANDBOX
|
|
47
|
+
os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
|
|
48
|
+
os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
|
|
49
|
+
_POLICY = os.path.join(_SANDBOX, "policy.json")
|
|
50
|
+
with open(_POLICY, "w", encoding="utf-8") as _f: # audit 闸 ACCEPT 面
|
|
51
|
+
json.dump({"forbidden": [], "required": ["B2MARK"]}, _f)
|
|
52
|
+
os.environ["MDCG_POLICY_FILE"] = _POLICY
|
|
53
|
+
os.environ.pop("MDCG_TEST_LIVE_ROOT", None)
|
|
54
|
+
|
|
55
|
+
from . import crypto, mdcg, mcp_server, nodefile # noqa: E402
|
|
56
|
+
from .mdcos import MdCGSecure # noqa: E402
|
|
57
|
+
from .security import Principal # noqa: E402
|
|
58
|
+
|
|
59
|
+
_KEK = os.urandom(32)
|
|
60
|
+
_MARK = "B2MARK 私有正文:银行卡尾号 8888,仅本人可见"
|
|
61
|
+
_OK = 0
|
|
62
|
+
_BAD = []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _check(name, cond, detail=""):
|
|
66
|
+
global _OK
|
|
67
|
+
if cond:
|
|
68
|
+
_OK += 1
|
|
69
|
+
print(" ok %s" % name)
|
|
70
|
+
else:
|
|
71
|
+
_BAD.append(name)
|
|
72
|
+
print(" FAIL %s %s" % (name, detail))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _principal():
|
|
76
|
+
return Principal(tenant="t1", actor="alice", role="designer",
|
|
77
|
+
clearance="secret", can_write=True, can_admin=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _mk_secure(tag):
|
|
81
|
+
root = os.path.join(_SANDBOX, "lib_" + tag)
|
|
82
|
+
return MdCGSecure(root, principal=_principal(), master_key=_KEK), root
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _raw(cg, nid):
|
|
86
|
+
"""读节点盘上原文(不经解密),返回 (text, fm, content)。"""
|
|
87
|
+
e = cg.index["nodes"][nid]
|
|
88
|
+
with open(cg._node_disk_path(e), encoding="utf-8") as f:
|
|
89
|
+
text = f.read()
|
|
90
|
+
fm, content = nodefile.loads(text)
|
|
91
|
+
return text, fm, content
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main():
|
|
95
|
+
print("\n[B2-A] cg(op=write, sensitivity='private') 端到端落盘")
|
|
96
|
+
cg_a, _root_a = _mk_secure("A")
|
|
97
|
+
res = mcp_server._dispatch(cg_a, "cg",
|
|
98
|
+
{"op": "write", "content_kind": "text",
|
|
99
|
+
"content": _MARK, "sensitivity": "private"})
|
|
100
|
+
nid_a = res.get("id") or ""
|
|
101
|
+
_check("B2-A1 写面回执 committed(未被审核队列吞掉)",
|
|
102
|
+
res.get("ok") is True and res.get("committed") is True,
|
|
103
|
+
repr(res)[:140])
|
|
104
|
+
if nid_a and nid_a in cg_a.index["nodes"]:
|
|
105
|
+
raw_a, fm_a, content_a = _raw(cg_a, nid_a)
|
|
106
|
+
_check("B2-A2 落盘 frontmatter sensitivity == 声明值 private",
|
|
107
|
+
fm_a.get("sensitivity") == "private",
|
|
108
|
+
"fm sensitivity=%r" % fm_a.get("sensitivity"))
|
|
109
|
+
_check("B2-A3 落盘正文不是明文(AEAD 密文)",
|
|
110
|
+
crypto.is_encrypted(content_a), str(content_a)[:40])
|
|
111
|
+
_check("B2-A4 盘上文件不含明文标记",
|
|
112
|
+
"B2MARK" not in raw_a and "8888" not in raw_a, raw_a[:60])
|
|
113
|
+
readback = (cg_a.get(nid_a) or {}).get("content") or ""
|
|
114
|
+
_check("B2-A5 有权限读面读回明文(加密存储不损可读性)",
|
|
115
|
+
_MARK in readback, readback[:40])
|
|
116
|
+
else:
|
|
117
|
+
_check("B2-A2 落盘 frontmatter sensitivity == 声明值 private", False,
|
|
118
|
+
"未落盘 id=%r" % nid_a)
|
|
119
|
+
|
|
120
|
+
print("\n[B2-B] 声明加密级却落盘明文 → fail-closed 拒绝")
|
|
121
|
+
dek = crypto.provision_dek(_SANDBOX, _KEK, "t1", "alice")
|
|
122
|
+
sealed = crypto.seal_node("payload", dek, "n1", "t1", "alice")
|
|
123
|
+
try:
|
|
124
|
+
mdcg._check_sensitivity_landing("n1", "internal", "private", "明文")
|
|
125
|
+
_check("B2-B1 闸①:声明 private 而落盘 internal → ValueError", False,
|
|
126
|
+
"未抛")
|
|
127
|
+
except ValueError as exc:
|
|
128
|
+
msg = str(exc)
|
|
129
|
+
_check("B2-B1 闸①:声明 private 而落盘 internal → ValueError",
|
|
130
|
+
"声明值='private'" in msg and "sensitivity='internal'" in msg
|
|
131
|
+
and "未写盘" in msg, msg[:110])
|
|
132
|
+
try:
|
|
133
|
+
mdcg._check_sensitivity_landing("n1", "private", "private", "明文", True)
|
|
134
|
+
_check("B2-B2 闸②:加密级密级 + 有密封能力却明文 → ValueError", False,
|
|
135
|
+
"未抛")
|
|
136
|
+
except ValueError as exc:
|
|
137
|
+
_check("B2-B2 闸②:加密级密级 + 有密封能力却明文 → ValueError",
|
|
138
|
+
"明文落盘" in str(exc), str(exc)[:110])
|
|
139
|
+
ok_pass = None
|
|
140
|
+
try:
|
|
141
|
+
mdcg._check_sensitivity_landing("n1", "private", "private", sealed, True)
|
|
142
|
+
mdcg._check_sensitivity_landing("n1", "internal", None, "明文", True)
|
|
143
|
+
ok_pass = True
|
|
144
|
+
except Exception as exc: # noqa: BLE001
|
|
145
|
+
ok_pass = repr(exc)
|
|
146
|
+
_check("B2-B3 合规面放行(密文 private / 明文 internal)", ok_pass is True,
|
|
147
|
+
repr(ok_pass)[:110])
|
|
148
|
+
|
|
149
|
+
# 端到端:密封点被绕过的实例(覆写了密封钩子却不密封)经写面写入 → 拒绝
|
|
150
|
+
class _LeakySecure(MdCGSecure):
|
|
151
|
+
def _seal_content(self, node_id, content, sensitivity=None):
|
|
152
|
+
return content # 故意不密封
|
|
153
|
+
|
|
154
|
+
leak = _LeakySecure(os.path.join(_SANDBOX, "lib_leak"),
|
|
155
|
+
principal=_principal(), master_key=_KEK)
|
|
156
|
+
err_e = None
|
|
157
|
+
try:
|
|
158
|
+
mcp_server._dispatch(leak, "cg",
|
|
159
|
+
{"op": "write", "content_kind": "text",
|
|
160
|
+
"content": _MARK, "sensitivity": "private"})
|
|
161
|
+
except ValueError as exc:
|
|
162
|
+
err_e = str(exc)
|
|
163
|
+
_check("B2-B4 写面端到端:密封点被绕过 + 声明 private → ValueError",
|
|
164
|
+
err_e is not None and "明文落盘" in err_e, repr(err_e)[:130])
|
|
165
|
+
leak_md = []
|
|
166
|
+
for _d, _subs, _fs in os.walk(os.path.join(_SANDBOX, "lib_leak")):
|
|
167
|
+
leak_md += [os.path.join(_d, f) for f in _fs if f.endswith(".md")]
|
|
168
|
+
_check("B2-B5 闸命中时未落盘(盘上无任何节点文件)", not leak_md,
|
|
169
|
+
"leak 库 .md=%s index=%s" % (leak_md[:3],
|
|
170
|
+
list(leak.index["nodes"])[:4]))
|
|
171
|
+
|
|
172
|
+
# 无密封能力的实例(基类 MdCG):private 明文是既有语义,不得误伤
|
|
173
|
+
base_root = os.path.join(_SANDBOX, "lib_base")
|
|
174
|
+
base = mdcg.MdCG(base_root, autoflush=1)
|
|
175
|
+
err_f = None
|
|
176
|
+
try:
|
|
177
|
+
base.add("b2_base_priv", _MARK, layer="knowledge",
|
|
178
|
+
sensitivity="private")
|
|
179
|
+
except Exception as exc: # noqa: BLE001
|
|
180
|
+
err_f = "%s: %s" % (type(exc).__name__, str(exc)[:90])
|
|
181
|
+
if err_f is None:
|
|
182
|
+
raw_f, fm_f, content_f = _raw(base, "b2_base_priv")
|
|
183
|
+
_check("B2-B6 无密封能力实例(基类):private 明文落盘放行(不误伤)",
|
|
184
|
+
fm_f.get("sensitivity") == "private"
|
|
185
|
+
and not crypto.is_encrypted(content_f),
|
|
186
|
+
"fm=%r enc=%s" % (fm_f.get("sensitivity"),
|
|
187
|
+
crypto.is_encrypted(content_f)))
|
|
188
|
+
else:
|
|
189
|
+
_check("B2-B6 无密封能力实例(基类):private 明文落盘放行(不误伤)",
|
|
190
|
+
False, err_f)
|
|
191
|
+
base.close()
|
|
192
|
+
|
|
193
|
+
print("\n[B2-C] 第二处落盘面:mdcg_remember 非 gated 分支透传")
|
|
194
|
+
cg_c, _root_c = _mk_secure("C")
|
|
195
|
+
res_c = mcp_server._dispatch(cg_c, "mdcg_remember",
|
|
196
|
+
{"content": _MARK + " remember 分支",
|
|
197
|
+
"content_kind": "text",
|
|
198
|
+
"sensitivity": "private"})
|
|
199
|
+
nid_c = res_c.get("id") or ""
|
|
200
|
+
_check("B2-C1 remember 面回执 ok", res_c.get("ok") is True,
|
|
201
|
+
repr(res_c)[:120])
|
|
202
|
+
if nid_c and nid_c in cg_c.index["nodes"]:
|
|
203
|
+
raw_c, fm_c, content_c = _raw(cg_c, nid_c)
|
|
204
|
+
_check("B2-C2 remember 面落盘 fm sensitivity == private",
|
|
205
|
+
fm_c.get("sensitivity") == "private",
|
|
206
|
+
"fm sensitivity=%r" % fm_c.get("sensitivity"))
|
|
207
|
+
_check("B2-C3 remember 面落盘正文为密文",
|
|
208
|
+
crypto.is_encrypted(content_c), str(content_c)[:40])
|
|
209
|
+
else:
|
|
210
|
+
_check("B2-C2 remember 面落盘 fm sensitivity == private", False,
|
|
211
|
+
"未落盘 id=%r" % nid_c)
|
|
212
|
+
|
|
213
|
+
print("\n==== B2 密级落盘守卫:%d 通过%s ====" % (
|
|
214
|
+
_OK, ",%d 失败:%s" % (len(_BAD), "; ".join(_BAD)) if _BAD else ""))
|
|
215
|
+
return 1 if _BAD else 0
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
try:
|
|
220
|
+
_rc = main()
|
|
221
|
+
finally:
|
|
222
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
223
|
+
sys.exit(_rc)
|