@furongjun1999/dsh-memory 0.6.0 → 0.7.0
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 +618 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/README.md +1 -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/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/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/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -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//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -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_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/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- 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 +52 -2
- 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 +100 -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 +57 -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//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- 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//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -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/lib/bridge.js +24 -2
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.js +16 -1
- 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_e2e_locomo_qa.py +11 -4
- 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 +48 -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/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/freshness.py +511 -0
- package/md_cg/fsutil.py +454 -8
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- 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 +4261 -3818
- package/md_cg/mdcg.py +4695 -3547
- package/md_cg/mdcos.py +4672 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +713 -576
- 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/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/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- 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 +1436 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_auto_defaults.py +424 -0
- 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_boundary_hit.py +410 -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_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- 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 +1387 -0
- package/md_cg/test_hive_ingest.py +285 -285
- 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 +519 -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_n230_dirty_replay.py +375 -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 +4 -3
- 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_p2_six_elements.py +463 -0
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- 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 +522 -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_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -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/aeis_core/time_core.py +8 -0
- 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 +4 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/hooks.ts +229 -16
- package/src/init.ts +15 -1
- package/src/lib/prompt_safety.ts +88 -1
- package/utf8_boot.py +237 -0
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""N230 `_dirty` 重放遮蔽守卫(P0-3:新记录必须能盖住旧记录,旧不得盖新)。
|
|
3
|
+
|
|
4
|
+
缺陷(材料自编 N226,`docs/eval/缺陷挖掘_自主迭代_v25.md:132/:178` 记为**留档未修**):
|
|
5
|
+
|
|
6
|
+
站点 `md_cg/mdcg.py` 的 `_maybe_reload_index`(改动前 :1353-1357)——
|
|
7
|
+
`for nid, e in self._dirty.items(): … idx["nodes"][nid] = e` **无条件**把本实例
|
|
8
|
+
未 flush 的 `_dirty` 条目盖回重载后的索引上,没有任何时序/代际比较。
|
|
9
|
+
|
|
10
|
+
**探针实跑复现(2026-10-01,独立 tempfile 沙箱)**:
|
|
11
|
+
|
|
12
|
+
① A = MdCG(R, autoflush=1000);A.add("x_node", 正文=OLD) ⇒ A._dirty 含 x_node(未 flush)
|
|
13
|
+
② B = MdCG(R, autoflush=1);B.add("x_node", 正文=NEW);B.close() ⇒ 盘面与快照都是 NEW
|
|
14
|
+
③ A._maybe_reload_index() 返回 **True**(确实重载了),但重载后
|
|
15
|
+
A.index["nodes"]["x_node"]["content_hash"] **仍是 OLD**;新建读者实例 C 读到 NEW
|
|
16
|
+
⇒ 索引与盘面撕裂(检索面按旧值给候选/桶/指纹)
|
|
17
|
+
|
|
18
|
+
**修法(本轮)**:给 `_dirty` 引入「**落盘逐字见证**」——标脏时记下节点文件的
|
|
19
|
+
`(st_mtime_ns, st_size)`;重放前再 stat 一次,失配 ⇒ 本实例这条已被盘面更新盖过
|
|
20
|
+
⇒ **跳过重放**(放行盘面)。证不出陈旧(无 path / stat 失败 / 未挂 root)时照旧
|
|
21
|
+
重放——「本实例未落盘写入不因重载从检索面消失」这条既有不变量一并保留。
|
|
22
|
+
tombstone(`None`)语义**一字不改**(`_unstage` 立即 flush,不跨重载存活)。
|
|
23
|
+
|
|
24
|
+
断言分组:
|
|
25
|
+
|
|
26
|
+
G0 前置——站点在位:`_dirty_entry_superseded` / `_note_witness` 各一处;`_dirty.root` 已挂。
|
|
27
|
+
G1 **红基线可出示**(探针三步复现)——旧记录**不得**盖住盘面新记录:A 重载后
|
|
28
|
+
必须收敛到 NEW(= 新建读者实例 C 的读数),且 `_dirty_replay_superseded ≥ 1`。
|
|
29
|
+
G2 不变量保持——他进程只动**别的**节点时,本实例未 flush 的写入必须仍在检索面
|
|
30
|
+
上(`_dirty_replay_superseded == 0`,该条目仍在 index 且哈希不变)。
|
|
31
|
+
G3 tombstone 语义一字不改——`_dirty[nid]=None` 在重载时仍 pop 掉该条目。
|
|
32
|
+
G4 见证判据的判别力(正/负对照)——`_dirty_entry_superseded` 的四条边界逐一断言。
|
|
33
|
+
G5 单点结构(静态)——`for nid, e in self._dirty.items():` 的重放体内**必须**
|
|
34
|
+
经过 `_dirty_entry_superseded`(副本回归/裸赋值回归即红)。
|
|
35
|
+
|
|
36
|
+
运行:python -X utf8 -m md_cg.test_n230_dirty_replay
|
|
37
|
+
python -X utf8 -m md_cg.test_n230_dirty_replay --head-baseline # 红基线自证
|
|
38
|
+
python -X utf8 -m md_cg.test_n230_dirty_replay --mutate # 定点变异自证
|
|
39
|
+
python -X utf8 -m md_cg.test_n230_dirty_replay --mutate --list
|
|
40
|
+
|
|
41
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
42
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到——实现改了却没同步本表)。
|
|
43
|
+
|
|
44
|
+
**基线纪律**:`--head-baseline` 把 `_dirty_entry_superseded` 换回「恒 False」
|
|
45
|
+
(= 改动前的无条件重放),G1 必须转红;若不红,说明 G1 没有判别力。
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import contextlib
|
|
50
|
+
import io
|
|
51
|
+
import os
|
|
52
|
+
import shutil
|
|
53
|
+
import sys
|
|
54
|
+
import tempfile
|
|
55
|
+
import types
|
|
56
|
+
|
|
57
|
+
from .mdcg import MdCG
|
|
58
|
+
|
|
59
|
+
_PASS = []
|
|
60
|
+
_FAIL = []
|
|
61
|
+
_SKIP = []
|
|
62
|
+
_SRC = {}
|
|
63
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_n230_")
|
|
64
|
+
_GEN = [0]
|
|
65
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
66
|
+
|
|
67
|
+
BODY = "# 功能名:%s\n# 生效条件:全时窗\n# 子功能:s\n# 执行:%s\n" \
|
|
68
|
+
"# 验证方式:test\n# 不适用条件:无\n"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ok(cond, msg, extra=""):
|
|
72
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
73
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
74
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _rel_text(rel: str) -> str:
|
|
78
|
+
if rel in _SRC:
|
|
79
|
+
return _SRC[rel]
|
|
80
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
81
|
+
return f.read()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _sandbox(name: str) -> str:
|
|
85
|
+
_GEN[0] += 1
|
|
86
|
+
d = os.path.join(_TMP, "gen%d_%s" % (_GEN[0], name))
|
|
87
|
+
os.makedirs(d, exist_ok=True)
|
|
88
|
+
return d
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _disk_body(root: str, nid: str) -> str:
|
|
92
|
+
p = os.path.join(root, "knowledge", "orphan", nid + ".md")
|
|
93
|
+
return open(p, encoding="utf-8").read()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------- G0
|
|
97
|
+
def g0():
|
|
98
|
+
print("== G0 站点在位(前置)==")
|
|
99
|
+
src = _rel_text("md_cg/mdcg.py")
|
|
100
|
+
ok(src.count("def _dirty_entry_superseded") == 1,
|
|
101
|
+
"G0a mdcg.py 内 `_dirty_entry_superseded` 恰一处定义")
|
|
102
|
+
ok(src.count("def _note_witness") == 1,
|
|
103
|
+
"G0b mdcg.py 内 `_note_witness` 恰一处定义")
|
|
104
|
+
ok("self._dirty.root = self.root" in src,
|
|
105
|
+
"G0c MdCG.__init__ 把数据根交给脏集(见证要靠它拼绝对路径)")
|
|
106
|
+
r = _sandbox("g0")
|
|
107
|
+
cg = MdCG(r, autoflush=1000)
|
|
108
|
+
ok(cg._dirty.root == cg.root and os.path.isabs(cg._dirty.root),
|
|
109
|
+
"G0d 实例构造后 _dirty.root = 数据根", cg._dirty.root)
|
|
110
|
+
ok(cg._dirty_replay_superseded == 0,
|
|
111
|
+
"G0e 读数 _dirty_replay_superseded 初始为 0")
|
|
112
|
+
cg.close()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------- G1
|
|
116
|
+
def _probe_repro():
|
|
117
|
+
"""探针三步复现(返回值供 G1 逐条断言)。"""
|
|
118
|
+
r = _sandbox("g1")
|
|
119
|
+
a = MdCG(r, autoflush=1000)
|
|
120
|
+
a.add("x_node", BODY % ("x", "OLD"), layer="knowledge",
|
|
121
|
+
verification_basis="test")
|
|
122
|
+
witness = dict(a._dirty.staged_stat)
|
|
123
|
+
b = MdCG(r, autoflush=1)
|
|
124
|
+
b.add("x_node", BODY % ("x", "NEW"), layer="knowledge",
|
|
125
|
+
verification_basis="test")
|
|
126
|
+
b.close() # 他进程写出新盘面 + 新快照
|
|
127
|
+
reloaded = a._maybe_reload_index()
|
|
128
|
+
c = MdCG(r, autoflush=1000) # 新建读者实例(只读盘面)
|
|
129
|
+
out = {"root": r, "a": a, "c": c, "reloaded": reloaded,
|
|
130
|
+
"a_hash": (a.index["nodes"].get("x_node") or {}).get("content_hash"),
|
|
131
|
+
"c_hash": (c.index["nodes"].get("x_node") or {}).get("content_hash"),
|
|
132
|
+
"superseded": a._dirty_replay_superseded,
|
|
133
|
+
"witness": witness, "dirty": list(a._dirty),
|
|
134
|
+
"disk": _disk_body(r, "x_node")}
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def g1():
|
|
139
|
+
print("== G1 红基线可出示:旧记录不得盖住盘面新记录(探针复现)==")
|
|
140
|
+
R = _probe_repro()
|
|
141
|
+
ok(R["witness"].get("x_node") is not None,
|
|
142
|
+
"G1 前置:A 标脏时记下了逐字见证 (mtime_ns, size)", R["witness"])
|
|
143
|
+
ok(R["reloaded"] is True,
|
|
144
|
+
"G1a 前置:重载确已发生(_maybe_reload_index() == True)", R["reloaded"])
|
|
145
|
+
ok("执行:NEW" in R["disk"],
|
|
146
|
+
"G1b 前置:盘面真值是 NEW", R["disk"].splitlines()[3])
|
|
147
|
+
ok(R["c_hash"] is not None and R["a_hash"] == R["c_hash"],
|
|
148
|
+
"G1c **新盖旧**:A 重载后条目 == 新建读者实例 C 的条目(收敛到盘面)",
|
|
149
|
+
{"A": R["a_hash"], "C": R["c_hash"]})
|
|
150
|
+
ok(R["superseded"] >= 1,
|
|
151
|
+
"G1d 可判读数:至少 1 条被判「已被盘面更新盖过」而跳过重放",
|
|
152
|
+
R["superseded"])
|
|
153
|
+
ok(R["a"]._dirty.get("x_node") is not None,
|
|
154
|
+
"G1e 该条仍在 _dirty(本实例确实还没 flush——不是靠 flush 凑的绿)",
|
|
155
|
+
list(R["a"]._dirty))
|
|
156
|
+
R["a"].close(); R["c"].close()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------- G2
|
|
160
|
+
def g2():
|
|
161
|
+
print("== G2 不变量保持:他进程只动别的节点时,未落盘写入不消失 ==")
|
|
162
|
+
r = _sandbox("g2")
|
|
163
|
+
a = MdCG(r, autoflush=1000)
|
|
164
|
+
a.add("y_node", BODY % ("y", "MINE"), layer="knowledge",
|
|
165
|
+
verification_basis="test")
|
|
166
|
+
a_hash = a.index["nodes"]["y_node"]["content_hash"]
|
|
167
|
+
b = MdCG(r, autoflush=1)
|
|
168
|
+
b.add("z_node", BODY % ("z", "OTHER"), layer="knowledge",
|
|
169
|
+
verification_basis="test")
|
|
170
|
+
b.close()
|
|
171
|
+
reloaded = a._maybe_reload_index()
|
|
172
|
+
ok(reloaded is True, "G2 前置:重载确已发生(他进程改了签名)", reloaded)
|
|
173
|
+
ok(a._dirty_replay_superseded == 0,
|
|
174
|
+
"G2a y_node 未被判陈旧(盘面上没人动它)——见证失配才是陈旧",
|
|
175
|
+
a._dirty_replay_superseded)
|
|
176
|
+
e = a.index["nodes"].get("y_node")
|
|
177
|
+
ok(e is not None and e.get("content_hash") == a_hash,
|
|
178
|
+
"G2b **不变量保持**:本实例未 flush 的 y_node 仍在检索面上且哈希不变",
|
|
179
|
+
{"present": e is not None})
|
|
180
|
+
ok(a.index["nodes"].get("z_node") is not None,
|
|
181
|
+
"G2c 他进程的 z_node 也可见(重载不该只保一边)")
|
|
182
|
+
a.close(); b.close()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------- G3
|
|
186
|
+
def g3():
|
|
187
|
+
print("== G3 tombstone 语义一字不改 ==")
|
|
188
|
+
r = _sandbox("g3")
|
|
189
|
+
c = MdCG(r, autoflush=1000)
|
|
190
|
+
c.add("tomb_node", BODY % ("t", "X"), layer="knowledge",
|
|
191
|
+
verification_basis="test")
|
|
192
|
+
c.flush() # 入日志;_dirty 清空
|
|
193
|
+
ok("tomb_node" in c.index["nodes"], "G3 前置:节点已在索引里")
|
|
194
|
+
c.index["nodes"].pop("tomb_node", None)
|
|
195
|
+
c._dirty["tomb_node"] = None # 不经 _unstage(那条路立即 flush)
|
|
196
|
+
ok(c._dirty.get("tomb_node") is None and "tomb_node" in c._dirty,
|
|
197
|
+
"G3a 前置:_dirty 里有该 nid 的 tombstone(值 None)")
|
|
198
|
+
c._index_sig = None # 强制签名失配 → 下次读面必然重载
|
|
199
|
+
ok(c._maybe_reload_index() is True, "G3b 前置:已重载")
|
|
200
|
+
ok("tomb_node" not in c.index["nodes"],
|
|
201
|
+
"G3c tombstone 仍被重放 pop 掉(本修复不碰 tombstone 语义)")
|
|
202
|
+
c.close()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------- G4
|
|
206
|
+
def g4():
|
|
207
|
+
print("== G4 见证判据的判别力(正/负对照)==")
|
|
208
|
+
r = _sandbox("g4")
|
|
209
|
+
c = MdCG(r, autoflush=1000)
|
|
210
|
+
c.add("w_node", BODY % ("w", "V1"), layer="knowledge",
|
|
211
|
+
verification_basis="test")
|
|
212
|
+
ok(c._dirty_entry_superseded("w_node") is False,
|
|
213
|
+
"G4a 负对照:标脏后盘面未动 → 不判陈旧(未落盘写入照旧重放)")
|
|
214
|
+
import time as _t
|
|
215
|
+
_t.sleep(0.01)
|
|
216
|
+
with open(os.path.join(r, c._dirty["w_node"]["path"]), "w",
|
|
217
|
+
encoding="utf-8", newline="\n") as f:
|
|
218
|
+
f.write(BODY % ("w", "V2"))
|
|
219
|
+
ok(c._dirty_entry_superseded("w_node") is True,
|
|
220
|
+
"G4b 正对照:盘面被改写 → 判陈旧(旧不得盖新)")
|
|
221
|
+
os.remove(os.path.join(r, c._dirty["w_node"]["path"]))
|
|
222
|
+
ok(c._dirty_entry_superseded("w_node") is True,
|
|
223
|
+
"G4c 正对照:文件被删掉(标脏时在)→ 判陈旧")
|
|
224
|
+
c._dirty.staged_stat.pop("w_node", None)
|
|
225
|
+
ok(c._dirty_entry_superseded("w_node") is False,
|
|
226
|
+
"G4d 无见证(证不出陈旧)→ 不判陈旧——不变量优先于覆盖面")
|
|
227
|
+
c._dirty.staged_stat["nopath"] = (1, 1)
|
|
228
|
+
c._dirty["nopath"] = {"layer": "knowledge"} # 无 path 的条目
|
|
229
|
+
ok(c._dirty_entry_superseded("nopath") is False,
|
|
230
|
+
"G4e 条目无 path → 不判陈旧(无见证可查)")
|
|
231
|
+
ok(c._dirty.root == r and c._dirty.staged_stat.get("w_node") is None,
|
|
232
|
+
"G4f 移除文件的那次 stat 失败**不登记**见证(staged_stat 无该键)")
|
|
233
|
+
c.close()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------- G5
|
|
237
|
+
def g5():
|
|
238
|
+
print("== G5 单点结构(静态:重放体必须过判据)==")
|
|
239
|
+
src = _rel_text("md_cg/mdcg.py")
|
|
240
|
+
i = src.index("for nid, e in self._dirty.items():")
|
|
241
|
+
body = src[i:i + 900]
|
|
242
|
+
ok("self._dirty_entry_superseded(nid)" in body,
|
|
243
|
+
"G5 重放循环体内经过 `_dirty_entry_superseded`(副本回归即红)")
|
|
244
|
+
ok("superseded += 1" in body and "continue" in body,
|
|
245
|
+
"G5a 判为陈旧即 `continue`(跳过重放,不是只记账)")
|
|
246
|
+
# 可执行行里不得再有无条件覆盖的裸赋值形态
|
|
247
|
+
code = "\n".join(l for l in body.splitlines()
|
|
248
|
+
if not l.strip().startswith("#"))
|
|
249
|
+
ok(code.count('idx["nodes"][nid] = e') == 1,
|
|
250
|
+
"G5b 重放体里 `idx[\"nodes\"][nid] = e` 恰一处(在判据之后)")
|
|
251
|
+
ok("self._dirty_replay_superseded = superseded" in src,
|
|
252
|
+
"G5c 跳过条数落在可判读数 `_dirty_replay_superseded` 上")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
_GROUPS = (g0, g1, g2, g3, g4, g5)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _run_groups() -> int:
|
|
259
|
+
_PASS.clear()
|
|
260
|
+
_FAIL.clear()
|
|
261
|
+
for g in _GROUPS:
|
|
262
|
+
try:
|
|
263
|
+
g()
|
|
264
|
+
except Exception as exc: # noqa: BLE001
|
|
265
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
266
|
+
import traceback
|
|
267
|
+
print(traceback.format_exc()[-900:])
|
|
268
|
+
return len(_FAIL)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------- 变异表
|
|
272
|
+
# 锚点 = (名字, rel, old, new)。每次变异必须让套件**转红**:
|
|
273
|
+
# M1 重放体恢复无条件覆盖(= 改动前形态)→ G1c/G1d/G5 红
|
|
274
|
+
# M2 判据恒 False(见证照记但不采信)→ G1c/G1d 红
|
|
275
|
+
# M3 见证完全不登记 → 判据永远拿不到见证 ⇒ 同 M2
|
|
276
|
+
_MUTATIONS = (
|
|
277
|
+
("重放体恢复无条件覆盖(改动前形态)", "md_cg/mdcg.py",
|
|
278
|
+
" if self._dirty_entry_superseded(nid):\n"
|
|
279
|
+
" superseded += 1\n continue",
|
|
280
|
+
" if False:\n superseded += 1\n continue"),
|
|
281
|
+
("判据恒 False(见证不采信)", "md_cg/mdcg.py",
|
|
282
|
+
" return (cur.st_mtime_ns, cur.st_size) != st",
|
|
283
|
+
" return False"),
|
|
284
|
+
("见证完全不登记", "md_cg/mdcg.py",
|
|
285
|
+
" self.staged_stat[k] = (st.st_mtime_ns, st.st_size)",
|
|
286
|
+
" self.staged_stat.pop(k, None)"),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
291
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
292
|
+
"__file__": os.path.join(_REPO, rel)}
|
|
293
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
294
|
+
m = types.ModuleType(name)
|
|
295
|
+
m.__dict__.update(ns)
|
|
296
|
+
return m
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
300
|
+
print("!! 定点变异自证:逐条把判据改回「改动前/失效」形态,套件必须转红\n")
|
|
301
|
+
if list_only:
|
|
302
|
+
for name, rel, _o, _n in _MUTATIONS:
|
|
303
|
+
print(" %-34s [%s]" % (name, rel))
|
|
304
|
+
return 0
|
|
305
|
+
anchor_miss, bad = [], []
|
|
306
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
307
|
+
clean = _run_groups()
|
|
308
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
309
|
+
if clean:
|
|
310
|
+
bad.append("未变异基线即失败")
|
|
311
|
+
|
|
312
|
+
for name, rel, old, new in _MUTATIONS:
|
|
313
|
+
src = _rel_text(rel)
|
|
314
|
+
if old not in src:
|
|
315
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没同步"
|
|
316
|
+
"本表)" % (name, rel))
|
|
317
|
+
anchor_miss.append(name)
|
|
318
|
+
continue
|
|
319
|
+
mut_src = src.replace(old, new, 1)
|
|
320
|
+
_SRC[rel] = mut_src
|
|
321
|
+
try:
|
|
322
|
+
mut = _exec_module("md_cg._n230_mut", rel, mut_src)
|
|
323
|
+
globals()["MdCG"] = mut.MdCG
|
|
324
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
325
|
+
reds = _run_groups()
|
|
326
|
+
detail = buf.getvalue()
|
|
327
|
+
finally:
|
|
328
|
+
globals()["MdCG"] = _REAL_MdCG
|
|
329
|
+
_SRC.pop(rel, None)
|
|
330
|
+
red_lines = [l for l in detail.splitlines()
|
|
331
|
+
if l.strip().startswith("FAIL ")]
|
|
332
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
333
|
+
print(" %s %-34s 红项=%d %s"
|
|
334
|
+
% ("OK " if reds else "MISS ", name, reds, verdict))
|
|
335
|
+
for l in red_lines[:5]:
|
|
336
|
+
print(" " + l.strip()[5:])
|
|
337
|
+
if not reds:
|
|
338
|
+
bad.append(name)
|
|
339
|
+
|
|
340
|
+
if anchor_miss:
|
|
341
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
342
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
343
|
+
return 2
|
|
344
|
+
print("\n定点变异自证:%s"
|
|
345
|
+
% ("PASS(每处判据都被打红)" if not bad else "FAIL —— " + "、".join(bad)))
|
|
346
|
+
return 0 if not bad else 1
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
_REAL_MdCG = MdCG
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def main() -> int:
|
|
353
|
+
try:
|
|
354
|
+
if "--mutate" in sys.argv:
|
|
355
|
+
return _mutate_mode("--list" in sys.argv)
|
|
356
|
+
if "--head-baseline" in sys.argv:
|
|
357
|
+
print("!! 红基线模式:_dirty_entry_superseded 换回「恒 False」"
|
|
358
|
+
"(= 改动前的无条件重放),G1 应当转红\n")
|
|
359
|
+
MdCG._dirty_entry_superseded = lambda self, nid: False
|
|
360
|
+
n = _run_groups()
|
|
361
|
+
print("\n== 红基线判定:应有失败 ==")
|
|
362
|
+
print("红基线失败数 = %d(>0 才算断言有判别力)" % n)
|
|
363
|
+
for f in _FAIL:
|
|
364
|
+
print(" 红:", f)
|
|
365
|
+
return 0 if n else 1
|
|
366
|
+
n = _run_groups()
|
|
367
|
+
print("\nN230 `_dirty` 重放遮蔽守卫:%d 通过,%d 失败,%d 跳过"
|
|
368
|
+
% (len(_PASS), n, len(_SKIP)))
|
|
369
|
+
return 0 if not n else 1
|
|
370
|
+
finally:
|
|
371
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
sys.exit(main())
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""N62 守卫(第 5 轮,2026-09-27):令牌路径租户上限夹紧 + 租户绑定覆盖 fail-closed。
|
|
3
|
+
|
|
4
|
+
留档复测(v8 N62 首报 → v9/v15/v16 历轮成立 → 本轮第 5 次,medium 维持):
|
|
5
|
+
|
|
6
|
+
病灶①(cap 夹紧从未接线):verify_token 令牌路径返回 Principal 时
|
|
7
|
+
clearance=rec["clearance"] 原样透出(tokens.py:450)——TenantRegistry.cap_of /
|
|
8
|
+
principal_for 现成(security.py:323/332)但令牌路径零调用;tenantC 登记上限
|
|
9
|
+
internal 时 designer(secret) 令牌仍以 secret 运行(可读含 private/secret)。
|
|
10
|
+
|
|
11
|
+
病灶②(绑定覆盖 warn-only):verify_token 形参 tenant(MCP 侧来自
|
|
12
|
+
MDCG_TENANT)与令牌记录 rec["tenant"] 不同时仅 stderr 告警(tokens.py:441-446),
|
|
13
|
+
形参值照常顶替(:448 tenant=tenant or ...)——tenantA 令牌在 MDCG_TENANT=
|
|
14
|
+
tenantB 下以原权限对 tenantB 运行。
|
|
15
|
+
|
|
16
|
+
红守卫(哑临时登记表 + 哑令牌库 + 临时根,确定性、零真实状态;登记表经
|
|
17
|
+
MDCG_TENANT_REGISTRY 注入——与 _resolve_root 同源,无新形参,旧代码红为
|
|
18
|
+
功能红而非 TypeError):
|
|
19
|
+
R1 ② 覆盖被拒:tenantA designer 令牌 + tenant="tenantB" → TokenError
|
|
20
|
+
(fail-closed),消息含两侧租户与校正指引;
|
|
21
|
+
R2 ① cap 夹紧:tenantC(登记 cap=internal)designer 令牌(角色默认 secret)
|
|
22
|
+
无覆盖 → clearance 夹紧为 internal,allows("private"/"secret") 均 False;
|
|
23
|
+
R3 未登记租户零闸变:tenantD(不在登记表)令牌 clearance 原样;
|
|
24
|
+
R4 无冲突零误伤:tenant=None/""/同值 → 原令牌租户、零异常;
|
|
25
|
+
R5 MCP 进程面:_build_principal 同面复现(env 注入 MDCG_TOKEN + MDCG_TENANT
|
|
26
|
+
+ MDCG_TENANT_REGISTRY)——② 拒绝、① 夹紧、同值零误伤。
|
|
27
|
+
|
|
28
|
+
同面搭车(N174,low,见 deferred,本轮不修):mcp_server.py:3522-3528 legacy
|
|
29
|
+
env 路径 clearance 夹紧另行 deferred。
|
|
30
|
+
|
|
31
|
+
运行:python -m md_cg.test_n62_tenant_bind_failclosed
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import contextlib
|
|
36
|
+
import io
|
|
37
|
+
import os
|
|
38
|
+
import shutil
|
|
39
|
+
import sys
|
|
40
|
+
import tempfile
|
|
41
|
+
|
|
42
|
+
from . import tokens
|
|
43
|
+
from .security import TenantRegistry
|
|
44
|
+
|
|
45
|
+
PASS = FAIL = 0
|
|
46
|
+
FAILS = []
|
|
47
|
+
MARK = "租户绑定被入参覆盖" # fail-closed 拒绝消息稳定锚点
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check(name, cond, detail=""):
|
|
51
|
+
global PASS, FAIL
|
|
52
|
+
if cond:
|
|
53
|
+
PASS += 1
|
|
54
|
+
print(" OK " + name)
|
|
55
|
+
else:
|
|
56
|
+
FAIL += 1
|
|
57
|
+
FAILS.append(name)
|
|
58
|
+
print(" FAIL " + name + " " + str(detail)[:110])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _verify(token, tenant=None, path=None):
|
|
62
|
+
"""verify_token 捕获异常,返回 (principal|None, exc|None)。"""
|
|
63
|
+
try:
|
|
64
|
+
return tokens.verify_token(token, tenant=tenant, path=path), None
|
|
65
|
+
except tokens.TokenError as e:
|
|
66
|
+
return None, e
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
71
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_n62_")
|
|
72
|
+
tf = os.path.join(tmp, "_tokens.json")
|
|
73
|
+
reg_path = os.path.join(tmp, "_tenants.json")
|
|
74
|
+
old_reg = os.environ.get("MDCG_TENANT_REGISTRY")
|
|
75
|
+
try:
|
|
76
|
+
# 哑临时登记表:确定性上限(tenantA=secret 不夹、tenantB/tenantC=internal)
|
|
77
|
+
reg = TenantRegistry(reg_path)
|
|
78
|
+
reg.register("tenantA", os.path.join(tmp, "rootA"), clearance_cap="secret",
|
|
79
|
+
description="N62 守卫哑登记 tenantA")
|
|
80
|
+
reg.register("tenantB", os.path.join(tmp, "rootB"),
|
|
81
|
+
clearance_cap="internal", description="N62 哑登记 tenantB")
|
|
82
|
+
reg.register("tenantC", os.path.join(tmp, "rootC"),
|
|
83
|
+
clearance_cap="internal", description="N62 哑登记 tenantC")
|
|
84
|
+
os.environ["MDCG_TENANT_REGISTRY"] = reg_path
|
|
85
|
+
d_a = tokens.issue("designer", actor="dummy-designer-a", tenant="tenantA",
|
|
86
|
+
path=tf) # 角色默认 clearance=secret
|
|
87
|
+
d_c = tokens.issue("designer", actor="dummy-designer-c", tenant="tenantC",
|
|
88
|
+
path=tf) # 登记 cap=internal < secret
|
|
89
|
+
d_d = tokens.issue("designer", actor="dummy-designer-d", tenant="tenantD",
|
|
90
|
+
path=tf) # tenantD 未登记
|
|
91
|
+
|
|
92
|
+
# ---------- R1 ② 覆盖 fail-closed ----------
|
|
93
|
+
print("== R1 租户绑定被入参覆盖 → 拒绝(fail-closed)==")
|
|
94
|
+
p1, e1 = _verify(d_a["token"], tenant="tenantB", path=tf)
|
|
95
|
+
check("R1a 覆盖被拒(TokenError 且含锚点)",
|
|
96
|
+
p1 is None and e1 is not None and MARK in str(e1),
|
|
97
|
+
f"p={p1} exc={type(e1).__name__}: {str(e1)[:80]}")
|
|
98
|
+
check("R1b 拒绝消息含两侧租户与校正指引",
|
|
99
|
+
e1 is not None and "tenantA" in str(e1) and "tenantB" in str(e1)
|
|
100
|
+
and "MDCG_TENANT" in str(e1), str(e1)[:100])
|
|
101
|
+
|
|
102
|
+
# ---------- R2 ① cap 夹紧(登记租户,纯收紧) ----------
|
|
103
|
+
print("== R2 登记租户上限夹紧(cap=internal < 令牌 secret)==")
|
|
104
|
+
p2, e2 = _verify(d_c["token"], path=tf)
|
|
105
|
+
check("R2a clearance 夹紧为 internal",
|
|
106
|
+
e2 is None and p2 is not None and p2.clearance == "internal",
|
|
107
|
+
f"clearance={getattr(p2, 'clearance', None)} err={e2}")
|
|
108
|
+
check("R2b 可读不含 private/secret",
|
|
109
|
+
p2 is not None and not p2.allows("private")
|
|
110
|
+
and not p2.allows("secret"),
|
|
111
|
+
f"private={p2.allows('private') if p2 else None} "
|
|
112
|
+
f"secret={p2.allows('secret') if p2 else None}")
|
|
113
|
+
check("R2c internal 本级仍可读(收紧不误伤)",
|
|
114
|
+
p2 is not None and p2.allows("internal"),
|
|
115
|
+
f"internal={p2.allows('internal') if p2 else None}")
|
|
116
|
+
|
|
117
|
+
# ---------- R3 未登记租户零闸变 ----------
|
|
118
|
+
print("== R3 未登记租户零闸变(fresh install 行为不变)==")
|
|
119
|
+
p3, e3 = _verify(d_d["token"], path=tf)
|
|
120
|
+
check("R3 未登记 tenantD clearance 原样(不夹紧)",
|
|
121
|
+
e3 is None and p3 is not None and p3.clearance == "secret"
|
|
122
|
+
and p3.tenant == "tenantD" and p3.allows("secret"),
|
|
123
|
+
f"clearance={getattr(p3, 'clearance', None)} "
|
|
124
|
+
f"tenant={getattr(p3, 'tenant', None)} err={e3}")
|
|
125
|
+
|
|
126
|
+
# ---------- R4 无冲突零误伤 ----------
|
|
127
|
+
print("== R4 无冲突路径零误伤 ==")
|
|
128
|
+
for label, arg in (("None", None), ("空串", ""), ("同值", "tenantA")):
|
|
129
|
+
p4, e4 = _verify(d_a["token"], tenant=arg, path=tf)
|
|
130
|
+
check(f"R4 tenant={label}:零异常且回落令牌租户 tenantA",
|
|
131
|
+
e4 is None and p4 is not None and p4.tenant == "tenantA"
|
|
132
|
+
and p4.clearance == "secret",
|
|
133
|
+
f"tenant={getattr(p4, 'tenant', None)} "
|
|
134
|
+
f"clearance={getattr(p4, 'clearance', None)} err={e4}")
|
|
135
|
+
|
|
136
|
+
# ---------- R5 MCP 进程面(_build_principal 同面) ----------
|
|
137
|
+
print("== R5 MCP 进程级:MDCG_TOKEN + MDCG_TENANT + MDCG_TENANT_REGISTRY ==")
|
|
138
|
+
from .mcp_server import _build_principal
|
|
139
|
+
saved = {k: os.environ.get(k) for k in
|
|
140
|
+
("MDCG_TOKEN", "MDCG_TENANT", "MDCG_TOKEN_FILE", "MDCG_ROOT",
|
|
141
|
+
"MDCG_TENANT_REGISTRY", "MDCG_LEGACY_ENV_AUTH",
|
|
142
|
+
"MDCG_LEGACY_ENV_ADMIN", "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE",
|
|
143
|
+
"MDCG_CLEARANCE", "MDCG_ACTOR")}
|
|
144
|
+
try:
|
|
145
|
+
os.environ["MDCG_TOKEN"] = d_a["token"]
|
|
146
|
+
os.environ["MDCG_TOKEN_FILE"] = tf
|
|
147
|
+
os.environ["MDCG_ROOT"] = os.path.join(tmp, "mcp")
|
|
148
|
+
os.environ["MDCG_TENANT_REGISTRY"] = reg_path
|
|
149
|
+
os.environ["MDCG_TENANT"] = "tenantB"
|
|
150
|
+
for k in ("MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
|
|
151
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_CLEARANCE",
|
|
152
|
+
"MDCG_ACTOR"):
|
|
153
|
+
os.environ.pop(k, None)
|
|
154
|
+
buf = io.StringIO()
|
|
155
|
+
with contextlib.redirect_stderr(buf):
|
|
156
|
+
mp, merr = _build_principal()
|
|
157
|
+
check("R5a MCP 面覆盖被拒(err 含锚点,principal=None)",
|
|
158
|
+
mp is None and merr is not None and MARK in merr,
|
|
159
|
+
f"err={str(merr)[:90]}")
|
|
160
|
+
# ① 经 MCP 面:tenantC(登记 cap=internal)夹紧
|
|
161
|
+
os.environ["MDCG_TOKEN"] = d_c["token"]
|
|
162
|
+
os.environ["MDCG_TENANT"] = "tenantC"
|
|
163
|
+
mp2, merr2 = _build_principal()
|
|
164
|
+
check("R5b MCP 面 cap 夹紧(tenantC designer → internal)",
|
|
165
|
+
merr2 is None and mp2 is not None
|
|
166
|
+
and getattr(mp2, "clearance", None) == "internal",
|
|
167
|
+
f"clearance={getattr(mp2, 'clearance', None)} err={merr2}")
|
|
168
|
+
# 同值零误伤
|
|
169
|
+
os.environ["MDCG_TOKEN"] = d_a["token"]
|
|
170
|
+
os.environ["MDCG_TENANT"] = "tenantA"
|
|
171
|
+
mp3, merr3 = _build_principal()
|
|
172
|
+
check("R5c MDCG_TENANT 同值:零误伤正常通过",
|
|
173
|
+
merr3 is None and mp3 is not None
|
|
174
|
+
and getattr(mp3, "tenant", None) == "tenantA"
|
|
175
|
+
and getattr(mp3, "clearance", None) == "secret",
|
|
176
|
+
f"err={merr3} tenant={getattr(mp3, 'tenant', None)} "
|
|
177
|
+
f"clearance={getattr(mp3, 'clearance', None)}")
|
|
178
|
+
finally:
|
|
179
|
+
for k, v in saved.items():
|
|
180
|
+
if v is None:
|
|
181
|
+
os.environ.pop(k, None)
|
|
182
|
+
else:
|
|
183
|
+
os.environ[k] = v
|
|
184
|
+
finally:
|
|
185
|
+
if old_reg is None:
|
|
186
|
+
os.environ.pop("MDCG_TENANT_REGISTRY", None)
|
|
187
|
+
else:
|
|
188
|
+
os.environ["MDCG_TENANT_REGISTRY"] = old_reg
|
|
189
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
190
|
+
|
|
191
|
+
print()
|
|
192
|
+
if FAILS:
|
|
193
|
+
print("FAILED: %d 项 → %s" % (len(FAILS), ", ".join(FAILS)))
|
|
194
|
+
return 1
|
|
195
|
+
print("ALL OK: %d 项(N62 令牌路径 cap 夹紧 + 租户覆盖 fail-closed 守卫全绿)" % PASS)
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
sys.exit(main())
|