@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,307 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""N184 守卫(第 23 轮,2026-09-28):_keys.json 并发 provision 丢失更新——
|
|
3
|
+
陈旧快照整份写回无痕抹除他身份信封。
|
|
4
|
+
|
|
5
|
+
病灶(crypto.py:299-329 / :287-290,本轮 HEAD 分诊复核在位):
|
|
6
|
+
provision_dek 先 _load_keys 读快照(:306)、经 AEAD 签发后按该快照经
|
|
7
|
+
_save_keys(:328)整份写回(fsutil.atomic_write 仅 tmp+rename 防撕裂,
|
|
8
|
+
无文件锁、无写前对账)。两写者交错——A 读 t0 快照 → B 签发落盘 →
|
|
9
|
+
A 按 t0 陈旧快照写回——B 的信封无痕抹除:旧密文永久不可解,全程无
|
|
10
|
+
告警无备份无审计。N139(load_error 拒重签)只堵「损坏回落→静默重签」
|
|
11
|
+
形态,并发写回形态无闸。触发面无需攻击者:hive/exec.py:845 每次工具
|
|
12
|
+
调用新建 MdCGSecure → _init_crypto → provision_dek(mdcos.py:3694-3696),
|
|
13
|
+
worker 首次 lingshu_cg 与设计者会话首条 private 写并发即中招。
|
|
14
|
+
|
|
15
|
+
红守卫(哑 KEK=os.urandom(32) + 系统临时目录,零真实状态;对齐桩只在
|
|
16
|
+
写点对齐调度、读与签发全走真实代码路径,等价 OS 调度竞态窗口):
|
|
17
|
+
R1 交错复现(确定性):一次性 _load_keys 桩把 worker 的读定格在 t0
|
|
18
|
+
(designer 落盘前从盘上真实读到的空快照,不伪造任何数据)——
|
|
19
|
+
worker 后写不得抹除 designer 先签信封,designer 已密封节点必须
|
|
20
|
+
仍可解(修复前:envelopes 只剩 worker,解封/解密全灭);
|
|
21
|
+
R2 真并发跨身份(save 点 Barrier 对齐 × N 轮):双线程分别 provision
|
|
22
|
+
两个身份,任一轮任一信封丢失即红(修复前每轮必红;修复后被写锁
|
|
23
|
+
挡在临界区外,对齐窗自然超时属预期,捕获后照常落盘);
|
|
24
|
+
R2b 对齐桩自检:绕开写锁直呼写点,双写者必须同窗落盘——自证 R2 的
|
|
25
|
+
红条件有效(修复前的红不是假红);
|
|
26
|
+
R3 真并发同身份幂等:双线程同 (tenant, actor) 并发 provision——修后
|
|
27
|
+
临界区内互见,应得同一 DEK;修复前各签各的,一方 DEK 被抹;
|
|
28
|
+
R4 写前对账机制断言:_save_keys 盘面已有而快照没有的信封必须保留
|
|
29
|
+
(防线纵深,兜底任何陈旧快照写者);盘面 load_error 时拒写且
|
|
30
|
+
原字节不动(N139 同闸延申到写点);
|
|
31
|
+
R5 rotate 零误伤:rotate 换新密钥生效、同键以本次写回为准(不得被
|
|
32
|
+
对账复活旧信封),他身份信封不受 rotate 影响。
|
|
33
|
+
运行:python -m md_cg.test_n184_keys_concurrent_provision
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import contextlib
|
|
38
|
+
import io
|
|
39
|
+
import os
|
|
40
|
+
import shutil
|
|
41
|
+
import sys
|
|
42
|
+
import tempfile
|
|
43
|
+
import threading
|
|
44
|
+
|
|
45
|
+
from . import crypto
|
|
46
|
+
from .crypto import (LockedError, open_node, provision_dek, seal_node,
|
|
47
|
+
unwrap_dek)
|
|
48
|
+
|
|
49
|
+
PASS = FAIL = 0
|
|
50
|
+
FAILS = []
|
|
51
|
+
|
|
52
|
+
# 对齐窗超时必须短于 provision_dek 写锁超时(10s):修复后另一写者被锁
|
|
53
|
+
# 挡在临界区外、对齐自然超时(BrokenBarrierError),捕获后照常落盘,无死锁。
|
|
54
|
+
ALIGN_TIMEOUT = 0.3
|
|
55
|
+
ROUNDS = 12
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check(name, cond, detail=""):
|
|
59
|
+
global PASS, FAIL
|
|
60
|
+
if cond:
|
|
61
|
+
PASS += 1
|
|
62
|
+
print(" OK " + name)
|
|
63
|
+
else:
|
|
64
|
+
FAIL += 1
|
|
65
|
+
FAILS.append(name)
|
|
66
|
+
print(" FAIL " + name + " " + str(detail)[:110])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _unwrap(root, kek, tenant, actor):
|
|
70
|
+
try:
|
|
71
|
+
return unwrap_dek(root, kek, tenant, actor), None
|
|
72
|
+
except crypto.CryptoError as e:
|
|
73
|
+
return None, e
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read(p):
|
|
77
|
+
with open(p, "rb") as f:
|
|
78
|
+
return f.read()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _envelopes(root):
|
|
82
|
+
return set((_load_keys_real(root).get("envelopes") or {}).keys())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_load_keys_real = crypto._load_keys
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _AlignedSave:
|
|
89
|
+
"""save 点对齐桩:两个写者都到写点再放行(读与签发全真实路径)。
|
|
90
|
+
|
|
91
|
+
只对齐调度(等价 OS 竞态窗),不伪造任何数据。对齐未达成(修复后
|
|
92
|
+
被写锁挡在临界区外)属预期形态,BrokenBarrierError 吞掉照常落盘。
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, n=2):
|
|
96
|
+
self.barrier = threading.Barrier(n, timeout=ALIGN_TIMEOUT)
|
|
97
|
+
self.real = crypto._save_keys
|
|
98
|
+
self.aligned = 0
|
|
99
|
+
|
|
100
|
+
def __enter__(self):
|
|
101
|
+
crypto._save_keys = self._save
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def _save(self, root, data):
|
|
105
|
+
try:
|
|
106
|
+
self.barrier.wait()
|
|
107
|
+
self.aligned += 1
|
|
108
|
+
except threading.BrokenBarrierError:
|
|
109
|
+
self.barrier.abort()
|
|
110
|
+
return self.real(root, data)
|
|
111
|
+
|
|
112
|
+
def __exit__(self, *exc):
|
|
113
|
+
crypto._save_keys = self.real
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main():
|
|
118
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
119
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_n184_")
|
|
120
|
+
kek = os.urandom(32) # 哑 KEK:仅本守卫用,绝不触真实主密钥
|
|
121
|
+
try:
|
|
122
|
+
# ---------- R1 交错复现(确定性红) ----------
|
|
123
|
+
print("== R1 交错复现:worker 按 t0 陈旧快照写回不得抹除 designer 信封 ==")
|
|
124
|
+
root1 = os.path.join(tmp, "r1")
|
|
125
|
+
os.makedirs(root1)
|
|
126
|
+
t0_snapshot = _load_keys_real(root1) # t0:designer 落盘前的真实盘面
|
|
127
|
+
dek_d = provision_dek(root1, kek, "tenantA", "designer")
|
|
128
|
+
text = "设计者私有笔记:密钥与凭证管理要点"
|
|
129
|
+
sealed = seal_node(text, dek_d, "node1", "tenantA", "designer")
|
|
130
|
+
check("R1a designer 先签 + 密封成功",
|
|
131
|
+
dek_d is not None and "tenantA|designer" in _envelopes(root1))
|
|
132
|
+
# 一次性 _load_keys 桩:worker 的读定格在 t0(只此一读,其余真实)
|
|
133
|
+
state = {"used": False}
|
|
134
|
+
|
|
135
|
+
def frozen_once(root):
|
|
136
|
+
if not state["used"] and root == root1:
|
|
137
|
+
state["used"] = True
|
|
138
|
+
return t0_snapshot
|
|
139
|
+
return _load_keys_real(root)
|
|
140
|
+
|
|
141
|
+
crypto._load_keys = frozen_once
|
|
142
|
+
try:
|
|
143
|
+
dek_w = provision_dek(root1, kek, "tenantA", "hive-worker")
|
|
144
|
+
finally:
|
|
145
|
+
crypto._load_keys = _load_keys_real
|
|
146
|
+
check("R1b worker provision 完成(真实代码路径)", dek_w is not None)
|
|
147
|
+
env_after = _envelopes(root1)
|
|
148
|
+
check("R1c designer 信封幸存(整份写回不得抹除他身份信封)",
|
|
149
|
+
"tenantA|designer" in env_after,
|
|
150
|
+
f"envelopes={sorted(env_after)}")
|
|
151
|
+
dek_d2, e_d = _unwrap(root1, kek, "tenantA", "designer")
|
|
152
|
+
check("R1d designer DEK 仍可解封", dek_d2 == dek_d,
|
|
153
|
+
f"exc={e_d if e_d else '解封值不符'}")
|
|
154
|
+
# t3(证据场景):designer 重进(真实再签路径)——信封被抹时此处会
|
|
155
|
+
# 无告警静默重签一把新 DEK;t4:旧密文用重进所得 DEK 永久解不开。
|
|
156
|
+
dek_d3 = provision_dek(root1, kek, "tenantA", "designer")
|
|
157
|
+
check("R1e designer 重进取回同一 DEK(静默重签被阻断)",
|
|
158
|
+
dek_d3 == dek_d, f"重签={'是' if dek_d3 != dek_d else '否'}")
|
|
159
|
+
try:
|
|
160
|
+
opened = open_node(sealed, dek_d3, "node1", "tenantA", "designer")
|
|
161
|
+
except crypto.CryptoError:
|
|
162
|
+
opened = None
|
|
163
|
+
check("R1f designer 已密封节点仍可解(永久静默丢失被阻断)",
|
|
164
|
+
opened == text,
|
|
165
|
+
f"opened={'原文' if opened == text else 'None(AEAD 校验失败)'}")
|
|
166
|
+
dek_w2, e_w = _unwrap(root1, kek, "tenantA", "hive-worker")
|
|
167
|
+
check("R1g worker 自己的 DEK 也应可解封", dek_w2 == dek_w, f"exc={e_w}")
|
|
168
|
+
|
|
169
|
+
# ---------- R2 真并发跨身份(save 点对齐 × N 轮) ----------
|
|
170
|
+
print("== R2 真并发跨身份:双线程 provision 任一信封丢失即红 ==")
|
|
171
|
+
lost = []
|
|
172
|
+
aligned_rounds = 0
|
|
173
|
+
for i in range(ROUNDS):
|
|
174
|
+
root2 = os.path.join(tmp, f"r2_{i}")
|
|
175
|
+
os.makedirs(root2)
|
|
176
|
+
start = threading.Barrier(2, timeout=10)
|
|
177
|
+
out = {}
|
|
178
|
+
|
|
179
|
+
def _prov(actor):
|
|
180
|
+
start.wait()
|
|
181
|
+
out[actor] = provision_dek(root2, kek, f"t{i}", actor)
|
|
182
|
+
|
|
183
|
+
with _AlignedSave() as al:
|
|
184
|
+
ths = [threading.Thread(target=_prov, args=(a,),
|
|
185
|
+
daemon=True)
|
|
186
|
+
for a in ("designer", "hive-worker")]
|
|
187
|
+
for t in ths:
|
|
188
|
+
t.start()
|
|
189
|
+
for t in ths:
|
|
190
|
+
t.join(30)
|
|
191
|
+
aligned_rounds += 1 if al.aligned >= 1 else 0
|
|
192
|
+
want = {f"t{i}|designer", f"t{i}|hive-worker"}
|
|
193
|
+
got = _envelopes(root2)
|
|
194
|
+
for a in ("designer", "hive-worker"):
|
|
195
|
+
d, e = _unwrap(root2, kek, f"t{i}", a)
|
|
196
|
+
if a not in {g.split("|", 1)[1] for g in got} or d != out.get(a):
|
|
197
|
+
lost.append((i, a, str(e)))
|
|
198
|
+
check(f"R2 {ROUNDS} 轮并发零信封丢失(对齐达成 {aligned_rounds}/{ROUNDS} 轮)",
|
|
199
|
+
not lost, f"丢失明细={lost[:3]}")
|
|
200
|
+
|
|
201
|
+
# R2b 红条件有效性自证:绕开 provision_dek(即绕开写锁)直呼 _save_keys,
|
|
202
|
+
# 对齐桩必须能让双写者同窗落盘——证明 R2 的对齐机制本身有效(修复前
|
|
203
|
+
# R2 的红不是假红);修复后 R2 主断言里 aligned=0 恰是写锁挡住交错
|
|
204
|
+
# 的直接证据(第二轮被锁拦在临界区外,等不到对齐伙伴)。
|
|
205
|
+
print("== R2b 对齐桩自检:无锁直呼写点时双写者确能同窗落盘 ==")
|
|
206
|
+
root2b = os.path.join(tmp, "r2b")
|
|
207
|
+
os.makedirs(root2b)
|
|
208
|
+
start2b = threading.Barrier(2, timeout=10)
|
|
209
|
+
with _AlignedSave() as al2b:
|
|
210
|
+
|
|
211
|
+
def _save2b(tag):
|
|
212
|
+
start2b.wait()
|
|
213
|
+
crypto._save_keys(root2b, {"v": 1, "alg": crypto.ALG,
|
|
214
|
+
"envelopes": {f"t2b|{tag}": {}}})
|
|
215
|
+
|
|
216
|
+
ths = [threading.Thread(target=_save2b, args=(t,), daemon=True)
|
|
217
|
+
for t in ("a", "b")]
|
|
218
|
+
for t in ths:
|
|
219
|
+
t.start()
|
|
220
|
+
for t in ths:
|
|
221
|
+
t.join(30)
|
|
222
|
+
check("R2b 对齐桩确能让双写者同窗落盘(红条件有效性自证)",
|
|
223
|
+
al2b.aligned >= 1, f"aligned={al2b.aligned}")
|
|
224
|
+
|
|
225
|
+
# ---------- R3 真并发同身份幂等 ----------
|
|
226
|
+
print("== R3 真并发同身份:并发 provision 应互见幂等(同一 DEK) ==")
|
|
227
|
+
root3 = os.path.join(tmp, "r3")
|
|
228
|
+
os.makedirs(root3)
|
|
229
|
+
start3 = threading.Barrier(2, timeout=10)
|
|
230
|
+
out3 = {}
|
|
231
|
+
|
|
232
|
+
def _prov3(tag):
|
|
233
|
+
start3.wait()
|
|
234
|
+
out3[tag] = provision_dek(root3, kek, "t3", "alice")
|
|
235
|
+
|
|
236
|
+
with _AlignedSave() as al3:
|
|
237
|
+
ths = [threading.Thread(target=_prov3, args=(t,), daemon=True)
|
|
238
|
+
for t in ("a", "b")]
|
|
239
|
+
for t in ths:
|
|
240
|
+
t.start()
|
|
241
|
+
for t in ths:
|
|
242
|
+
t.join(30)
|
|
243
|
+
same = len(set(out3.values())) == 1 and None not in out3.values()
|
|
244
|
+
d3, e3 = _unwrap(root3, kek, "t3", "alice")
|
|
245
|
+
check("R3 并发同身份 provision 幂等(双方同一 DEK 且可解封)",
|
|
246
|
+
same and d3 in set(out3.values()),
|
|
247
|
+
f"deks={len(set(out3.values()))}种 exc={e3}")
|
|
248
|
+
|
|
249
|
+
# ---------- R4 写前对账机制断言 ----------
|
|
250
|
+
print("== R4 写前对账:盘面已有而快照没有的信封保留;load_error 拒写 ==")
|
|
251
|
+
root4 = os.path.join(tmp, "r4")
|
|
252
|
+
os.makedirs(root4)
|
|
253
|
+
dek_x = provision_dek(root4, kek, "t4", "xholder")
|
|
254
|
+
disk_env = _load_keys_real(root4)["envelopes"]
|
|
255
|
+
fake_y = {"id_fp": "0" * 16, "clearance": "private",
|
|
256
|
+
"nonce": "AAAA", "ct": "BBBB", "created_at": 0.0}
|
|
257
|
+
# 模拟陈旧快照写者:只带自己的 Y、不带盘面已有的 X
|
|
258
|
+
crypto._save_keys(root4, {"v": 1, "alg": crypto.ALG,
|
|
259
|
+
"envelopes": {"t4|ywriter": dict(fake_y)}})
|
|
260
|
+
env4 = _envelopes(root4)
|
|
261
|
+
check("R4a 盘面已有信封(X)在对账下幸存",
|
|
262
|
+
"t4|xholder" in env4, f"envelopes={sorted(env4)}")
|
|
263
|
+
check("R4b 写者自己的信封(Y)正常落盘", "t4|ywriter" in env4)
|
|
264
|
+
root4b = os.path.join(tmp, "r4b")
|
|
265
|
+
os.makedirs(root4b)
|
|
266
|
+
with open(crypto.keys_path(root4b), "wb") as f:
|
|
267
|
+
f.write(b'{"v": 1, "alg": "cha') # 哑损坏样本
|
|
268
|
+
corrupt4b = _read(crypto.keys_path(root4b))
|
|
269
|
+
buf4 = io.StringIO()
|
|
270
|
+
raised4b = None
|
|
271
|
+
with contextlib.redirect_stderr(buf4):
|
|
272
|
+
try:
|
|
273
|
+
crypto._save_keys(root4b, {"v": 1, "alg": crypto.ALG,
|
|
274
|
+
"envelopes": {}})
|
|
275
|
+
except LockedError as e:
|
|
276
|
+
raised4b = e
|
|
277
|
+
check("R4c 盘面 load_error 时写点拒绝(LockedError)",
|
|
278
|
+
raised4b is not None, str(raised4b)[:80])
|
|
279
|
+
check("R4d 损坏盘面字节不被覆盖(N139 同闸延申到写点)",
|
|
280
|
+
_read(crypto.keys_path(root4b)) == corrupt4b)
|
|
281
|
+
|
|
282
|
+
# ---------- R5 rotate 零误伤 ----------
|
|
283
|
+
print("== R5 rotate 零误伤:同键以本次写回为准,他身份信封不受影响 ==")
|
|
284
|
+
root5 = os.path.join(tmp, "r5")
|
|
285
|
+
os.makedirs(root5)
|
|
286
|
+
dek_a = provision_dek(root5, kek, "t5", "alice")
|
|
287
|
+
provision_dek(root5, kek, "t5", "bob")
|
|
288
|
+
dek_a2 = crypto.rotate_dek(root5, kek, "t5", "alice")
|
|
289
|
+
check("R5a rotate 换新密钥且生效",
|
|
290
|
+
dek_a2 != dek_a and _unwrap(root5, kek, "t5", "alice")[0] == dek_a2)
|
|
291
|
+
check("R5b rotate 不影响他身份信封",
|
|
292
|
+
_unwrap(root5, kek, "t5", "bob")[0] is not None
|
|
293
|
+
and "t5|bob" in _envelopes(root5))
|
|
294
|
+
finally:
|
|
295
|
+
crypto._load_keys = _load_keys_real
|
|
296
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
297
|
+
|
|
298
|
+
print()
|
|
299
|
+
if FAILS:
|
|
300
|
+
print("FAILED: %d 项 → %s" % (len(FAILS), ", ".join(FAILS)))
|
|
301
|
+
return 1
|
|
302
|
+
print("ALL OK: %d 项(N184 密钥库并发 provision 丢失更新守卫全绿)" % PASS)
|
|
303
|
+
return 0
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
sys.exit(main())
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 写路径跨进程索引代际守卫(N195,多进程共享库形态)
|
|
3
|
+
|
|
4
|
+
缺陷(修复前):`_maybe_reload_index`(mdcg.py:1034)全仓仅 **5 处** 调用点,
|
|
5
|
+
**全在读面**(mdcg.py:2132/2397/2601、mdcos.py:767/1315)。写路径
|
|
6
|
+
`MdCG.add`(protect.guard_write 调用点 mdcg.py:1476、prev_entry 取
|
|
7
|
+
`self.index` mdcg.py:1607)与 `MdCGOS.forget`(mdcos.py:2356
|
|
8
|
+
`self.index["nodes"].get`)直读本进程内存索引 → 他进程(serve 常驻 +
|
|
9
|
+
review_cli / hive worker 写方)刚写入、刚保护的 self·anchor 节点对本进程
|
|
10
|
+
不可见 → 「不可覆盖」闸静默失效:不抛 ProtectionError、不落
|
|
11
|
+
`_protected_history` 快照、不写 `_protected_audit.jsonl`,旧正文不可恢复
|
|
12
|
+
且无痕;同根因次生面:`forget` 对他进程新建节点恒返回 not_found
|
|
13
|
+
(静默无操作)。行号按修复后(+12 行注释)记。
|
|
14
|
+
|
|
15
|
+
守卫(五组,全部临时库 + 哑主密钥,绝不触真实 ~/.mdcg):
|
|
16
|
+
⓿ 接线审计:写路径 add 确经签名探测(计数桩,修复前 = 0 次即红);
|
|
17
|
+
① 常驻 A + 独立 B 写 anchor 节点 + close(compact 落快照,review_cli 退出
|
|
18
|
+
同款)→ A.add 覆写必须被 ProtectionError 拒绝,盘上正文保持 B 的原文;
|
|
19
|
+
并复刻「禁用 reload = 修前病灶」分支,证明失效闸确由代际重载合上;
|
|
20
|
+
② 删除面:A.forget 同一节点必须被 ProtectionError 拒绝(而非静默
|
|
21
|
+
not_found 无操作);显式 override=True 后删除成功且 trash 落文件;
|
|
22
|
+
③ 留痕面:override=True 覆写必须写 `_protected_history/<id>/` 快照 +
|
|
23
|
+
`_protected_audit.jsonl` 记录(修复前闸静默失效 → 两者皆无);
|
|
24
|
+
④ prev_entry 面:他进程已置 converged 的节点被 A 覆写后不得静默打回
|
|
25
|
+
active(mdcg.py:1607 直读 self.index 的次生后果)。
|
|
26
|
+
|
|
27
|
+
运行:python -m md_cg.test_n195_writepath_reload
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import os
|
|
33
|
+
import shutil
|
|
34
|
+
import sys
|
|
35
|
+
import tempfile
|
|
36
|
+
|
|
37
|
+
from . import protect
|
|
38
|
+
from .mdcos import MdCGSecure
|
|
39
|
+
from .security import Principal
|
|
40
|
+
|
|
41
|
+
PASS = FAIL = 0
|
|
42
|
+
FAILS = []
|
|
43
|
+
|
|
44
|
+
DUMMY_KEY = "cd" * 32 # 哑主密钥:本进程绝不触真实 ~/.mdcg
|
|
45
|
+
|
|
46
|
+
BODY_B = "# 功能名:锚点探针\n# 正文:B 进程写入的身份锚点(不可篡改)"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check(name, cond, detail=""):
|
|
50
|
+
global PASS, FAIL
|
|
51
|
+
if cond:
|
|
52
|
+
PASS += 1
|
|
53
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
54
|
+
else:
|
|
55
|
+
FAIL += 1
|
|
56
|
+
FAILS.append(name)
|
|
57
|
+
print(f" [FAIL] {name} · {detail}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _principal(**kw):
|
|
61
|
+
d = dict(actor="n195", clearance="internal", can_write=True,
|
|
62
|
+
can_admin=True, role="designer", auth_mode="test")
|
|
63
|
+
d.update(kw)
|
|
64
|
+
return Principal(**d)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _counting(cg):
|
|
68
|
+
"""给 _maybe_reload_index 挂计数桩,返回 (计数 dict, 解挂函数)。"""
|
|
69
|
+
calls = {"probe": 0, "reload": 0}
|
|
70
|
+
orig = cg._maybe_reload_index
|
|
71
|
+
|
|
72
|
+
def counted():
|
|
73
|
+
calls["probe"] += 1
|
|
74
|
+
r = orig()
|
|
75
|
+
if r:
|
|
76
|
+
calls["reload"] += 1
|
|
77
|
+
return r
|
|
78
|
+
|
|
79
|
+
cg._maybe_reload_index = counted
|
|
80
|
+
return calls, (lambda: setattr(cg, "_maybe_reload_index", orig))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _disk(root, rel):
|
|
84
|
+
"""直接读盘上节点文件(不经任何实例内存态)。"""
|
|
85
|
+
from . import nodefile
|
|
86
|
+
p = os.path.join(root, *rel.split("/"))
|
|
87
|
+
if not os.path.exists(p):
|
|
88
|
+
return None
|
|
89
|
+
with open(p, encoding="utf-8") as f:
|
|
90
|
+
return nodefile.loads(f.read())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _has_node(root, nid):
|
|
94
|
+
"""在 root 下按 <layer>/.../<nid>.md 找盘上文件,返回相对路径或 None。"""
|
|
95
|
+
for dp, _d, files in os.walk(root):
|
|
96
|
+
for fn in files:
|
|
97
|
+
if fn == nid + ".md":
|
|
98
|
+
return os.path.relpath(os.path.join(dp, fn),
|
|
99
|
+
root).replace("\\", "/")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main():
|
|
104
|
+
roots = []
|
|
105
|
+
os.environ["MDCG_MASTER_KEY"] = DUMMY_KEY # 哑主密钥先行(纪律)
|
|
106
|
+
try:
|
|
107
|
+
# ---------- ⓿ + ① 写保护闸:常驻 A + 独立写方 B ----------
|
|
108
|
+
print("\n【①】常驻 A → 独立 B 写 anchor 节点 + close → A.add 覆写")
|
|
109
|
+
root1 = tempfile.mkdtemp(prefix="n195_1_")
|
|
110
|
+
roots.append(root1)
|
|
111
|
+
a = MdCGSecure(root1, principal=_principal())
|
|
112
|
+
a.add("n195_seed", "# 功能名:种子\n# 正文:常驻 A 初始记忆",
|
|
113
|
+
layer="knowledge")
|
|
114
|
+
a.flush()
|
|
115
|
+
b = MdCGSecure(root1, principal=_principal(actor="writer_b"))
|
|
116
|
+
b.add("n195_anchor_probe", BODY_B, layer="anchor")
|
|
117
|
+
b.close() # close → compact:写 _index.json
|
|
118
|
+
rel = _has_node(root1, "n195_anchor_probe")
|
|
119
|
+
check("盘面已有 B 写入的 anchor 节点(setup)", rel is not None,
|
|
120
|
+
"rel=%r" % rel)
|
|
121
|
+
check("setup:A 内存索引尚未见该节点(A 未发生任何读面调用)",
|
|
122
|
+
"n195_anchor_probe" not in (a.index.get("nodes") or {}))
|
|
123
|
+
|
|
124
|
+
# ⓿ 接线:写路径确经签名探测
|
|
125
|
+
calls, uncount = _counting(a)
|
|
126
|
+
raised = None
|
|
127
|
+
try:
|
|
128
|
+
a.add("n195_anchor_probe", "# 功能名:伪造\n# 正文:被覆写的伪造内容",
|
|
129
|
+
layer="anchor")
|
|
130
|
+
except protect.ProtectionError as e:
|
|
131
|
+
raised = e
|
|
132
|
+
finally:
|
|
133
|
+
uncount()
|
|
134
|
+
check("⓿ 接线:写路径 add 经 _maybe_reload_index 签名探测 ≥1 次",
|
|
135
|
+
calls["probe"] >= 1, str(calls))
|
|
136
|
+
# ① 红线:不可覆盖闸必须生效
|
|
137
|
+
check("① 绿:A.add 覆写他进程 anchor 节点被 ProtectionError 拒绝",
|
|
138
|
+
raised is not None, repr(raised)[:120])
|
|
139
|
+
disk = _disk(root1, rel) if rel else None
|
|
140
|
+
check("① 绿:盘上正文保持 B 的原文(未被伪造内容覆写)",
|
|
141
|
+
disk is not None and BODY_B in (disk[1] or ""),
|
|
142
|
+
repr((disk[1] if disk else None))[:100])
|
|
143
|
+
# ①b 复刻「禁用 reload = 修前病灶」:闸确由代际重载合上
|
|
144
|
+
b2 = MdCGSecure(root1, principal=_principal(actor="writer_b2"))
|
|
145
|
+
b2.add("n195_red_probe", BODY_B, layer="anchor")
|
|
146
|
+
b2.close()
|
|
147
|
+
orig_reload = a._maybe_reload_index
|
|
148
|
+
a._maybe_reload_index = lambda: False # 修前形态:写路径无重载
|
|
149
|
+
red = None
|
|
150
|
+
try:
|
|
151
|
+
a.add("n195_red_probe", "# 功能名:伪造\n# 正文:被覆写的伪造内容",
|
|
152
|
+
layer="anchor")
|
|
153
|
+
except protect.ProtectionError as e:
|
|
154
|
+
red = e
|
|
155
|
+
finally:
|
|
156
|
+
a._maybe_reload_index = orig_reload
|
|
157
|
+
check("①b 红形态复刻:禁用重载时闸静默失效(不抛 ProtectionError)",
|
|
158
|
+
red is None, repr(red)[:100])
|
|
159
|
+
rel_red = _has_node(root1, "n195_red_probe")
|
|
160
|
+
d2 = _disk(root1, rel_red) if rel_red else None
|
|
161
|
+
check("①b 红形态复刻:盘上正文已被伪造内容覆写(无痕覆写)",
|
|
162
|
+
d2 is not None and "被覆写的伪造内容" in (d2[1] or ""),
|
|
163
|
+
repr((d2[1] if d2 else None))[:80])
|
|
164
|
+
check("①b 红形态复刻:无保护历史、无保护审计(旧正文不可恢复)",
|
|
165
|
+
not os.path.isdir(os.path.join(root1, protect.HISTORY_DIR))
|
|
166
|
+
and not os.path.exists(os.path.join(root1, protect.AUDIT_FILE)))
|
|
167
|
+
|
|
168
|
+
# ---------- ② 删除面:forget ----------
|
|
169
|
+
print("\n【②】A.forget 他进程 anchor 节点")
|
|
170
|
+
b3 = MdCGSecure(root1, principal=_principal(actor="writer_b3"))
|
|
171
|
+
b3.add("n195_forget_probe", BODY_B, layer="anchor")
|
|
172
|
+
b3.close()
|
|
173
|
+
fr_raised = None
|
|
174
|
+
try:
|
|
175
|
+
a.forget("n195_forget_probe", reason="N195 删除面")
|
|
176
|
+
except protect.ProtectionError as e:
|
|
177
|
+
fr_raised = e
|
|
178
|
+
check("② 绿:A.forget 受保护节点被 ProtectionError 拒绝(非 not_found)",
|
|
179
|
+
fr_raised is not None, repr(fr_raised)[:120])
|
|
180
|
+
check("② 绿:拒绝后盘上文件仍在(未被静默删除)",
|
|
181
|
+
_has_node(root1, "n195_forget_probe") is not None)
|
|
182
|
+
fr2 = a.forget("n195_forget_probe", reason="N195 覆盖删除",
|
|
183
|
+
override=True)
|
|
184
|
+
check("② 绿:override=True 后删除成功(ok=True)",
|
|
185
|
+
isinstance(fr2, dict) and fr2.get("ok") is True, str(fr2)[:120])
|
|
186
|
+
rel_fr = _has_node(root1, "n195_forget_probe")
|
|
187
|
+
check("② 绿:删除后文件仅存于 trash/(anchor/ 侧已移出)",
|
|
188
|
+
rel_fr is not None and rel_fr.startswith("trash/"), repr(rel_fr))
|
|
189
|
+
check("② 留痕:override 删除写入 _protected_audit.jsonl",
|
|
190
|
+
os.path.exists(os.path.join(root1, protect.AUDIT_FILE))
|
|
191
|
+
and any(r.get("action") == "override_forget"
|
|
192
|
+
and r.get("node_id") == "n195_forget_probe"
|
|
193
|
+
for r in _jsonl(os.path.join(root1,
|
|
194
|
+
protect.AUDIT_FILE))))
|
|
195
|
+
|
|
196
|
+
# ---------- ③ 留痕面:override=True 覆写 ----------
|
|
197
|
+
print("\n【③】override=True 覆写受保护节点必须快照 + 审计")
|
|
198
|
+
b4 = MdCGSecure(root1, principal=_principal(actor="writer_b4"))
|
|
199
|
+
b4.add("n195_ovr_probe", BODY_B, layer="anchor")
|
|
200
|
+
b4.close()
|
|
201
|
+
ov = None
|
|
202
|
+
try:
|
|
203
|
+
ov = a.add("n195_ovr_probe", "# 功能名:覆写\n# 正文:显式覆盖后正文",
|
|
204
|
+
layer="anchor", override=True)
|
|
205
|
+
except Exception as e: # noqa: BLE001
|
|
206
|
+
ov = "EXC:%r" % (e,)
|
|
207
|
+
check("③ 覆写成功(override=True 放行)", ov == "n195_ovr_probe",
|
|
208
|
+
repr(ov)[:120])
|
|
209
|
+
hd = os.path.join(root1, protect.HISTORY_DIR, "n195_ovr_probe")
|
|
210
|
+
check("③ 绿:旧版本快照落 _protected_history/<id>/(可恢复)",
|
|
211
|
+
os.path.isdir(hd) and [f for f in os.listdir(hd)
|
|
212
|
+
if f.endswith(".md")],
|
|
213
|
+
"history=%s" % (os.listdir(hd) if os.path.isdir(hd) else None))
|
|
214
|
+
check("③ 绿:覆写留痕写进 _protected_audit.jsonl(override_write)",
|
|
215
|
+
any(r.get("action") == "override_write"
|
|
216
|
+
and r.get("node_id") == "n195_ovr_probe"
|
|
217
|
+
for r in _jsonl(os.path.join(root1, protect.AUDIT_FILE))))
|
|
218
|
+
|
|
219
|
+
# ---------- ④ prev_entry 面:生命周期状态继承 ----------
|
|
220
|
+
print("\n【④】他进程 converged 节点被 A 覆写后不得静默打回 active")
|
|
221
|
+
b5 = MdCGSecure(root1, principal=_principal(actor="writer_b5"))
|
|
222
|
+
b5.add("n195_state_probe", "# 功能名:状态探针\n# 正文:已定型节点",
|
|
223
|
+
layer="knowledge")
|
|
224
|
+
st = b5.set_state("n195_state_probe", "converged", reason="N195 探针")
|
|
225
|
+
b5.flush()
|
|
226
|
+
b5.close()
|
|
227
|
+
check("setup:B 已把该节点置 converged",
|
|
228
|
+
st.get("ok") is True, str(st)[:100])
|
|
229
|
+
a.add("n195_state_probe", "# 功能名:状态探针\n# 正文:普通覆写一次",
|
|
230
|
+
layer="knowledge")
|
|
231
|
+
got = a.get("n195_state_probe") or {}
|
|
232
|
+
check("④ 绿:覆写继承 lifecycle_state=converged(未打回 active)",
|
|
233
|
+
(got.get("frontmatter") or {}).get("lifecycle_state")
|
|
234
|
+
== "converged",
|
|
235
|
+
repr((got.get("frontmatter") or {}).get("lifecycle_state")))
|
|
236
|
+
a.close()
|
|
237
|
+
|
|
238
|
+
# ---------- ⑤ 自身写入零额外重载(不回归性能/不自重载) ----------
|
|
239
|
+
print("\n【⑤】自身写入连续 add 不自触发重载")
|
|
240
|
+
root2 = tempfile.mkdtemp(prefix="n195_2_")
|
|
241
|
+
roots.append(root2)
|
|
242
|
+
c = MdCGSecure(root2, principal=_principal())
|
|
243
|
+
c.add("n195_own_seed", "# 功能名:自写种子\n# 正文:own", layer="knowledge")
|
|
244
|
+
c.flush()
|
|
245
|
+
c2, unc2 = _counting(c)
|
|
246
|
+
for i in range(3):
|
|
247
|
+
c.add("n195_own_%d" % i, "# 功能名:自写%d\n# 正文:own %d" % (i, i),
|
|
248
|
+
layer="knowledge")
|
|
249
|
+
c.flush()
|
|
250
|
+
unc2()
|
|
251
|
+
check("⑤ 自身写入(flush 只追加分片、不改快照签名)重载 0 次",
|
|
252
|
+
c2["reload"] == 0, str(c2))
|
|
253
|
+
check("⑤ 接线在位:3 次 add 经 ≥3 次签名探测",
|
|
254
|
+
c2["probe"] >= 3, str(c2))
|
|
255
|
+
c.close()
|
|
256
|
+
finally:
|
|
257
|
+
for r_ in roots:
|
|
258
|
+
shutil.rmtree(r_, ignore_errors=True)
|
|
259
|
+
|
|
260
|
+
print("\n" + "=" * 64)
|
|
261
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
262
|
+
if FAILS:
|
|
263
|
+
print("失败项:" + ";".join(FAILS))
|
|
264
|
+
return 0 if FAIL == 0 else 1
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _jsonl(p):
|
|
268
|
+
out = []
|
|
269
|
+
if not os.path.exists(p):
|
|
270
|
+
return out
|
|
271
|
+
with open(p, encoding="utf-8") as f:
|
|
272
|
+
for line in f:
|
|
273
|
+
line = line.strip()
|
|
274
|
+
if line:
|
|
275
|
+
try:
|
|
276
|
+
out.append(json.loads(line))
|
|
277
|
+
except ValueError:
|
|
278
|
+
pass
|
|
279
|
+
return out
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
sys.exit(main())
|