@furongjun1999/dsh-memory 0.6.0 → 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 -572
- 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/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/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_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/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_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/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 -3547
- package/md_cg/mdcos.py +4491 -4159
- 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/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_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 +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_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_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 +3 -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/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 「索引缺条目 → 闸静默跳过」跨进程守卫(N196)
|
|
3
|
+
|
|
4
|
+
与 N195 同根(`_maybe_reload_index` 未在**做闸判定**的路径上前置),但后果
|
|
5
|
+
落在**读隔离**与**授权**两面,与 N195 的写保护面不同:
|
|
6
|
+
|
|
7
|
+
① 读隔离(`MdCGSecure.get`,mdcos.py:4039):该实现先读
|
|
8
|
+
`self.index["nodes"].get(node_id)`,**缺条目即整段跳过 `_readable`**,
|
|
9
|
+
随后 `super().get()` 内部才重载。他进程(同 actor、异 session)刚写入
|
|
10
|
+
并 compact 的 private(会话绑定档)节点,在常驻实例的**首次** get 被
|
|
11
|
+
解出(第二次读才归 None——那时条目已随重载进索引)。
|
|
12
|
+
② 授权(`MdCGSecure.verify` falsified 分支,mdcos.py:3887):N130 的
|
|
13
|
+
`require_layer_write` 只在「索引中有该条目」时执行;他进程刚写入的
|
|
14
|
+
knowledge 层节点在常驻 verifier 实例索引中不存在 → 闸整段跳过 →
|
|
15
|
+
verifier(layers_allow=rejected/contextual)falsified 删掉 knowledge
|
|
16
|
+
层节点,盘上原文消失(先重载则 AccessDenied、原文保留)。
|
|
17
|
+
|
|
18
|
+
修复口径(与 N195 同款、fail-closed):闸判定前先 `_maybe_reload_index()`
|
|
19
|
+
——「本进程陈旧」与「节点真不存在」两态由此可区分;真不存在时的
|
|
20
|
+
not_found / None 语义不变。成本只在未命中路径(一次 stat)。
|
|
21
|
+
|
|
22
|
+
守卫(两组,全部临时库 + 哑主密钥,绝不触真实 ~/.mdcg):
|
|
23
|
+
① 常驻 A(actor=X, session=S1, 非 admin) → 独立写方 B(actor=X, session=S2)
|
|
24
|
+
写 private 节点并 close → A.get 必须 None(读隔离生效);
|
|
25
|
+
并断言条文条目在重载后确实为「会话绑定档」而非空壳;
|
|
26
|
+
② 常驻 V(verifier, layers_allow=rejected/contextual) → 独立写方 W 写
|
|
27
|
+
knowledge 节点并 close → V.verify(falsified) 必须 AccessDenied,
|
|
28
|
+
且盘上 knowledge 原文保留(修复前:删除成功、原文消失)。
|
|
29
|
+
|
|
30
|
+
运行:python -m md_cg.test_n196_stale_gate_skip
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import glob
|
|
35
|
+
import os
|
|
36
|
+
import shutil
|
|
37
|
+
import sys
|
|
38
|
+
import tempfile
|
|
39
|
+
|
|
40
|
+
from .mdcos import MdCGSecure
|
|
41
|
+
from .security import AccessDenied, Principal
|
|
42
|
+
|
|
43
|
+
PASS = FAIL = 0
|
|
44
|
+
FAILS = []
|
|
45
|
+
|
|
46
|
+
DUMMY_KEY = "ef" * 32 # 哑主密钥:本进程绝不触真实 ~/.mdcg
|
|
47
|
+
|
|
48
|
+
READER = "reader_x" # ① 读写同 actor、异 session(会话绑定档的正例)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check(name, cond, detail=""):
|
|
52
|
+
global PASS, FAIL
|
|
53
|
+
if cond:
|
|
54
|
+
PASS += 1
|
|
55
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
56
|
+
else:
|
|
57
|
+
FAIL += 1
|
|
58
|
+
FAILS.append(name)
|
|
59
|
+
print(f" [FAIL] {name} · {detail}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _principal(**kw):
|
|
63
|
+
d = dict(tenant="default", actor=READER, clearance="secret",
|
|
64
|
+
can_write=True, can_admin=False, role="designer",
|
|
65
|
+
auth_mode="test")
|
|
66
|
+
d.update(kw)
|
|
67
|
+
return Principal(**d)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _md_files(root, layer):
|
|
71
|
+
return [os.path.relpath(p, root).replace("\\", "/") for p in
|
|
72
|
+
glob.glob(os.path.join(root, layer, "**", "*.md"), recursive=True)]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main():
|
|
76
|
+
roots = []
|
|
77
|
+
os.environ["MDCG_MASTER_KEY"] = DUMMY_KEY # 哑主密钥先行(纪律)
|
|
78
|
+
try:
|
|
79
|
+
# ---------- ① 读隔离:陈旧索引不得跳过 _readable ----------
|
|
80
|
+
print("\n【①】常驻 A 读他进程写入的同 actor 异 session private 节点")
|
|
81
|
+
root1 = tempfile.mkdtemp(prefix="n196_1_")
|
|
82
|
+
roots.append(root1)
|
|
83
|
+
a = MdCGSecure(root1, principal=_principal(session="S1"))
|
|
84
|
+
a.add("n196_seed", "# 功能名:种子\n# 正文:A 初始记忆", layer="knowledge")
|
|
85
|
+
a.flush()
|
|
86
|
+
# 同 actor(READER)异 session:密文 AAD 只绑 (node_id, tenant, actor)
|
|
87
|
+
# ——actor 一致才能解封,故「读隔离」是这一腿**唯一**的拦截面。
|
|
88
|
+
b = MdCGSecure(root1, principal=_principal(session="S2"))
|
|
89
|
+
b.add("n196_priv", "# 功能名:私密\n# 正文:S2 的会话绑定档",
|
|
90
|
+
layer="knowledge", sensitivity="private")
|
|
91
|
+
b.close()
|
|
92
|
+
check("setup:A 索引尚未含该节点(A 未发生任何读面调用)",
|
|
93
|
+
"n196_priv" not in (a.index.get("nodes") or {}))
|
|
94
|
+
got = a.get("n196_priv")
|
|
95
|
+
check("① 绿:A.get 归 None(跨会话 private 未被解出)",
|
|
96
|
+
got is None, repr(got and got.get("content"))[:80])
|
|
97
|
+
a._maybe_reload_index()
|
|
98
|
+
e = (a.index.get("nodes") or {}).get("n196_priv") or {}
|
|
99
|
+
check("① 对照:重载后条目确为会话绑定档(非空壳,才谈得上被拦)",
|
|
100
|
+
e.get("session") == "S2"
|
|
101
|
+
and _rank_private(e.get("sensitivity")),
|
|
102
|
+
str({k: e.get(k) for k in ("session", "sensitivity")}))
|
|
103
|
+
check("① 对照:重载后再次 get 仍 None(口径一致)",
|
|
104
|
+
a.get("n196_priv") is None)
|
|
105
|
+
res, _ = a.search("会话绑定档")
|
|
106
|
+
check("① 对照:search 面同样不可见",
|
|
107
|
+
not any(n.get("id") == "n196_priv" for n, _s, _q in res),
|
|
108
|
+
str([n.get("id") for n, _s, _q in res])[:80])
|
|
109
|
+
# 同 session 读:绑定档正常可见(防「一律拒读」的过度收紧)
|
|
110
|
+
c = MdCGSecure(root1, principal=_principal(session="S2"))
|
|
111
|
+
own = c.get("n196_priv")
|
|
112
|
+
check("① 反向:同 session 实例可读(未过度收紧)",
|
|
113
|
+
own is not None, repr(own and own.get("content"))[:60])
|
|
114
|
+
c.close()
|
|
115
|
+
a.close()
|
|
116
|
+
|
|
117
|
+
# ---------- ② 授权:陈旧索引不得跳过 N130 层写闸 ----------
|
|
118
|
+
print("\n【②】常驻 verifier falsified 他进程新建的 knowledge 层节点")
|
|
119
|
+
root2 = tempfile.mkdtemp(prefix="n196_2_")
|
|
120
|
+
roots.append(root2)
|
|
121
|
+
v = MdCGSecure(root2, principal=_principal(
|
|
122
|
+
actor="verifier_1", role="verifier",
|
|
123
|
+
layers_allow=("rejected", "contextual")))
|
|
124
|
+
v.add("n196_ver_seed", "# 功能名:种子\n# 正文:verifier 自有",
|
|
125
|
+
layer="rejected")
|
|
126
|
+
v.flush()
|
|
127
|
+
w = MdCGSecure(root2, principal=_principal(actor="writer_w"))
|
|
128
|
+
w.add("n196_know_target", "# 功能名:被验证目标\n# 正文:knowledge 原文",
|
|
129
|
+
layer="knowledge")
|
|
130
|
+
w.close()
|
|
131
|
+
check("setup:V 索引尚未含该节点",
|
|
132
|
+
"n196_know_target" not in (v.index.get("nodes") or {}))
|
|
133
|
+
denied = None
|
|
134
|
+
try:
|
|
135
|
+
out = v.verify("n196_know_target", evidence="伪证", verdict="falsified")
|
|
136
|
+
denied = "RET:%r" % (out,)
|
|
137
|
+
except AccessDenied as e:
|
|
138
|
+
denied = e
|
|
139
|
+
check("② 绿:falsified 被 AccessDenied 拒绝(层写闸生效)",
|
|
140
|
+
isinstance(denied, AccessDenied), repr(denied)[:140])
|
|
141
|
+
check("② 绿:knowledge 层原文保留(未被 falsified 删除)",
|
|
142
|
+
_md_files(root2, "knowledge") == ["knowledge/orphan/n196_know_target.md"],
|
|
143
|
+
str(_md_files(root2, "knowledge")))
|
|
144
|
+
# 对照:真不存在时仍走 not_found 透传(未把闸改成无条件拒绝)
|
|
145
|
+
out2 = v.verify("n196_no_such_node", evidence="e", verdict="falsified")
|
|
146
|
+
check("② 反向:索引/盘面皆无此节点时仍透传 None(not_found 语义不变)",
|
|
147
|
+
out2 is None, repr(out2)[:80])
|
|
148
|
+
v.close()
|
|
149
|
+
finally:
|
|
150
|
+
for r_ in roots:
|
|
151
|
+
shutil.rmtree(r_, ignore_errors=True)
|
|
152
|
+
|
|
153
|
+
print("\n" + "=" * 64)
|
|
154
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
155
|
+
if FAILS:
|
|
156
|
+
print("失败项:" + ";".join(FAILS))
|
|
157
|
+
return 0 if FAIL == 0 else 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _rank_private(sens):
|
|
161
|
+
from .security import SENSITIVITY_ORDER
|
|
162
|
+
try:
|
|
163
|
+
return SENSITIVITY_ORDER.index(sens) >= SENSITIVITY_ORDER.index("private")
|
|
164
|
+
except ValueError:
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
sys.exit(main())
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 写面绕层闸族守卫(N197 + N208,按指令同批计一个席位)
|
|
3
|
+
|
|
4
|
+
缺陷(修复前,本守卫红态实跑):两条写面都**直调 `cg._write_node`**,既无
|
|
5
|
+
`principal.require_layer_write` 亦无 `protect.guard_write`——同一身份对**同层**
|
|
6
|
+
的 `add` 被正常 `AccessDenied`,但这两条写面照样落盘(「同一库两条出口口径
|
|
7
|
+
不一致」),且不抛错、不落 `_protected_history` 快照、不写
|
|
8
|
+
`_protected_audit.jsonl`:
|
|
9
|
+
|
|
10
|
+
① `md_cg/insight.py:297`(verify 写回):`_read_event` 后 `cg._write_node(...)`,
|
|
11
|
+
全程零闸。攻击者用 `sustain`(`layers_allow=('self',)`、`can_write=True`、
|
|
12
|
+
ops_allow 含 insight → `_insight_call` 只查 can_write)对**他人留下的
|
|
13
|
+
contextual 层** pending 事件调 `verify(verdict="verified", evidence=[])`
|
|
14
|
+
→ 越层改写他人层节点,零证据自证 + 自置不可遗忘保护位
|
|
15
|
+
(`importance` 抬到 0.9、`protected=True`、`protection_reason=insight_verified`、
|
|
16
|
+
盘面 `verification_basis="data"`)。
|
|
17
|
+
② `md_cg/ccgc.py:880`(recalibrate 写回,:804 的 link 同款):唯一准入是**自报
|
|
18
|
+
字符串** `verifier != compiled_by`(:839 `_same_subject`),无层闸、无保护闸
|
|
19
|
+
→ `record` 身份(layers_allow 不含 self)可改写 **self 层 immutable 节点**的
|
|
20
|
+
正文与 `condition_space`;且 :189-208 `_upsert_ccg_line` 不过滤换行——
|
|
21
|
+
`slot_corrections={"observation_tool": "工具B\\n# 功能名:注入的新功能名"}`
|
|
22
|
+
会在正文里**注入一行独立的 `# 功能名:…`**(`ccg_field_value` 取首个命中行
|
|
23
|
+
⇒ 读面被顶替)。
|
|
24
|
+
|
|
25
|
+
修复(最小改动 · 同族单点,镜像既有 N131 review 队列 merge 面的双闸顺序):
|
|
26
|
+
① `protect.guard_overwrite(cg, node_id, ...)`:既有节点覆写前的统一双闸——
|
|
27
|
+
`principal.require_layer_write(节点真层, 敏感度)` 在先(对照
|
|
28
|
+
`MdCGSecure.add`),引擎级 `protect.guard_write` 在后(对照 `MdCG.add`);
|
|
29
|
+
进入前做索引代际探活(`_maybe_reload_index`,N195 同族:陈旧索引会让两道
|
|
30
|
+
闸同时静默失效)。
|
|
31
|
+
② `insight.verify` 与 `ccgc.link` / `ccgc.recalibrate` 三个写点前统一过该闸
|
|
32
|
+
(`ccgc` 两处共用同一调用,避免「修一处漏一处」)。
|
|
33
|
+
③ `ccgc._upsert_ccg_line` 拒收含换行的值(`ValueError`),`compile_dialog`
|
|
34
|
+
与 `recalibrate` 在**更早的边界**以 `E022` 拒绝——fail-closed 且带结构化
|
|
35
|
+
错误码,不静默改写调用方的值。
|
|
36
|
+
|
|
37
|
+
守卫(六组,全部临时库 + 哑主密钥 + 临时令牌库,**绝不触真实 ~/.mdcg**):
|
|
38
|
+
⓿ 隔离自证:aux 根 / 令牌库都须落在守卫临时目录,否则中止;
|
|
39
|
+
① insight.verify:sustain 越层被拒(盘面零改写);designer 对 self 层事件节点
|
|
40
|
+
被**保护闸**拒(层闸对其放行,故该腿单独钉住 guard_write);record/reflect
|
|
41
|
+
同层正当写入仍成功(反向腿,防一刀切收紧);
|
|
42
|
+
② ccgc.recalibrate:record 改写 self 层节点被拒(层闸);designer 对同节点被
|
|
43
|
+
保护闸拒;换行注入被 `E022` 拒且盘面零改写、无伪造行;合法单行修正仍成功;
|
|
44
|
+
直调 `_upsert_ccg_line` 的注入被拒;
|
|
45
|
+
③ ccgc.link 同款注入面:显式 marks 带换行的候选在编译期即遭 `E022`,link
|
|
46
|
+
apply 拒写;合法单行候选 + 令牌签章仍落库(反向腿);
|
|
47
|
+
④ 工具面真身(与缺陷材料同口径):`mcp_server.call_tool(cg, "cg", …)` 走
|
|
48
|
+
`op=insight action=verify` / `op=ccgc action=recalibrate` 同样被拒;
|
|
49
|
+
⑤ 源断言 + floor:三处写点前都出现 `guard_overwrite`、`_write_node` 前有闸;
|
|
50
|
+
实弹断言计数下限。
|
|
51
|
+
|
|
52
|
+
实测(本机):修前 10 passed / 18 failed(用 HEAD 版 insight.py/ccgc.py/protect.py
|
|
53
|
+
覆盖临时包副本后实跑,18 条红腿含 4a/4b 两条工具面端到端);修后 28 passed /
|
|
54
|
+
0 failed(live=20)。
|
|
55
|
+
|
|
56
|
+
运行:python -X utf8 -m md_cg.test_n197_n208_write_face_gates
|
|
57
|
+
"""
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import atexit
|
|
61
|
+
import json
|
|
62
|
+
import os
|
|
63
|
+
import shutil
|
|
64
|
+
import sys
|
|
65
|
+
import tempfile
|
|
66
|
+
import time
|
|
67
|
+
|
|
68
|
+
# ── 隔离前置:必须在 import md_cg 子模块**之前**(tokens/theory 等在模块级把
|
|
69
|
+
# aux_root() 冻成常量)——否则会指到真实 ~/.mdcg 并在那里建密钥/令牌库。 ──
|
|
70
|
+
_TMP = tempfile.mkdtemp(prefix="n197_")
|
|
71
|
+
os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "auxroot")
|
|
72
|
+
os.environ["MDCG_MASTER_KEY"] = "cd" * 32 # 哑主密钥(绝不触真实密钥面)
|
|
73
|
+
os.environ["MDCG_TOKEN_FILE"] = os.path.join(_TMP, "tokens.json")
|
|
74
|
+
os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cgroot")
|
|
75
|
+
for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
|
|
76
|
+
"DSH_SESSION_ID", "MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
|
|
77
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_STATE_ROOT",
|
|
78
|
+
"MDCG_DATA_ROOT", "MDCG_TENANT_REGISTRY", "MDCG_VERIFIER_MODULES",
|
|
79
|
+
"PYTHONIOENCODING", "PYTHONUTF8"):
|
|
80
|
+
os.environ.pop(_k, None)
|
|
81
|
+
|
|
82
|
+
from . import ccgc, insight, mcp_server, nodefile, protect, tokens # noqa: E402
|
|
83
|
+
from .datapath import aux_root # noqa: E402
|
|
84
|
+
from .mdcos import MdCGOS, MdCGSecure # noqa: E402
|
|
85
|
+
from .security import Principal # noqa: E402
|
|
86
|
+
|
|
87
|
+
atexit.register(lambda: _cleanup(_TMP)) # LIFO:晚于库层 _atexit_flush_all
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
91
|
+
except AttributeError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
ROOT = os.path.join(_TMP, "cgroot")
|
|
95
|
+
PASS = FAIL = LIVE = 0
|
|
96
|
+
LIVE_FLOOR = 18
|
|
97
|
+
FAILS = []
|
|
98
|
+
|
|
99
|
+
C1_OK = {"retrievability": 0.8, "outside_observer": True,
|
|
100
|
+
"cross_domain": ["存储", "网络"], "premise_questioned": True,
|
|
101
|
+
"pressure": "low", "continuity_turns": 3, "externalized": True,
|
|
102
|
+
"tone": "curious"}
|
|
103
|
+
SLOTS = {"observation_position": "本地磁盘",
|
|
104
|
+
"time_window": [0, 253402300799],
|
|
105
|
+
"observation_tool": "扩展名映射表",
|
|
106
|
+
"existence_constraint": "仅对本地可读文件生效"}
|
|
107
|
+
INJECT = "工具B\n# 功能名:注入的新功能名" # 换行注入探针
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check(name, cond, detail="", live=False):
|
|
111
|
+
global PASS, FAIL, LIVE
|
|
112
|
+
if live:
|
|
113
|
+
LIVE += 1
|
|
114
|
+
if cond:
|
|
115
|
+
PASS += 1
|
|
116
|
+
print(" [PASS] %s" % name)
|
|
117
|
+
else:
|
|
118
|
+
FAIL += 1
|
|
119
|
+
FAILS.append(name)
|
|
120
|
+
print(" [FAIL] %s · %s" % (name, detail))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _princ(role, clearance="internal", can_admin=False):
|
|
124
|
+
spec = tokens.role_spec(role)
|
|
125
|
+
return Principal(tenant="default", actor="n197-" + role,
|
|
126
|
+
clearance=clearance, can_write=bool(spec["can_write"]),
|
|
127
|
+
can_admin=can_admin, role=role,
|
|
128
|
+
layers_allow=spec["layers_allow"],
|
|
129
|
+
ops_allow=spec["ops_allow"], auth_mode="test")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_OPEN = []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _secured(principal):
|
|
136
|
+
cg = MdCGSecure(ROOT, principal=principal)
|
|
137
|
+
_OPEN.append(cg)
|
|
138
|
+
return cg
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _close_all():
|
|
142
|
+
for cg in _OPEN:
|
|
143
|
+
try:
|
|
144
|
+
cg.close()
|
|
145
|
+
except Exception: # noqa: BLE001 —— 清理面不得再抛
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _cleanup(path) -> bool:
|
|
150
|
+
import gc
|
|
151
|
+
for _ in range(6):
|
|
152
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
153
|
+
if not os.path.exists(path):
|
|
154
|
+
return True
|
|
155
|
+
gc.collect()
|
|
156
|
+
time.sleep(0.25)
|
|
157
|
+
return not os.path.exists(path)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _fm_of(cg, nid):
|
|
161
|
+
node = cg.get(nid)
|
|
162
|
+
return (node or {}).get("frontmatter") or {}, (node or {}).get("content") or ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _raises(fn, *a, **kw):
|
|
166
|
+
"""执行并返回 (ok, 异常类名/空, 结果/异常文本)。"""
|
|
167
|
+
try:
|
|
168
|
+
return True, "", fn(*a, **kw)
|
|
169
|
+
except Exception as exc: # noqa: BLE001 —— 守卫要的是拒绝形态
|
|
170
|
+
return False, type(exc).__name__, str(exc)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _snapshot_state(cg, nid):
|
|
174
|
+
"""(frontmatter 关键位, 正文) —— 用于「盘面零改写」断言。"""
|
|
175
|
+
fm, content = _fm_of(cg, nid)
|
|
176
|
+
return ({k: fm.get(k) for k in ("insight_state", "protected",
|
|
177
|
+
"protection_reason", "importance",
|
|
178
|
+
"verification_basis", "condition_space")},
|
|
179
|
+
content)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _src_window(src, start, end):
|
|
183
|
+
i = src.find(start)
|
|
184
|
+
if i < 0:
|
|
185
|
+
return ""
|
|
186
|
+
j = src.find(end, i)
|
|
187
|
+
return src[i:] if j < 0 else src[i:j]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _not_raises_but_no_forged_line():
|
|
191
|
+
"""直调 _upsert_ccg_line:换行值必须被拒(抛异常)或至少不产生独立注入行。"""
|
|
192
|
+
try:
|
|
193
|
+
out = ccgc._upsert_ccg_line("# 正文\n", "生效条件", INJECT)
|
|
194
|
+
except ValueError:
|
|
195
|
+
return True
|
|
196
|
+
return not [ln for ln in out.split("\n") if ln.strip().startswith("# 功能名")]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main():
|
|
200
|
+
global LIVE
|
|
201
|
+
# ---------- ⓿ 隔离自证 ----------
|
|
202
|
+
print("[0] 隔离自证")
|
|
203
|
+
_tok = tokens.token_file()
|
|
204
|
+
check("0a 默认令牌库与 aux 根都落在守卫临时目录(绝不触真实凭据)",
|
|
205
|
+
os.path.abspath(_tok).lower().startswith(os.path.abspath(_TMP).lower())
|
|
206
|
+
and os.path.abspath(aux_root()).lower().startswith(
|
|
207
|
+
os.path.abspath(_TMP).lower()),
|
|
208
|
+
"tok=%s aux=%s" % (_tok, aux_root()))
|
|
209
|
+
if not os.path.abspath(ROOT).lower().startswith(os.path.abspath(_TMP).lower()):
|
|
210
|
+
print("FATAL: 隔离失败,中止(绝不带病跑真实库)")
|
|
211
|
+
return 2
|
|
212
|
+
|
|
213
|
+
# ---------- 夹具:contextual 洞见事件 ×2 + self 层事件 + 两节点 ----------
|
|
214
|
+
designer = _secured(_princ("designer", clearance="secret", can_admin=True))
|
|
215
|
+
ev = designer.insight(action="record", statement="N197 探针事件:零证据自证(甲链路)",
|
|
216
|
+
conditions=C1_OK)
|
|
217
|
+
ev_id = ev["node_id"]
|
|
218
|
+
ev2 = designer.insight(action="record",
|
|
219
|
+
statement="缓存分层应保持离线可运行:跨域证据待补(乙链路)",
|
|
220
|
+
conditions=C1_OK)
|
|
221
|
+
ev2_id = ev2["node_id"]
|
|
222
|
+
ev3 = designer.insight(action="record",
|
|
223
|
+
statement="离线优先的网络探测策略(丙链路·工具面探针)",
|
|
224
|
+
conditions=C1_OK)
|
|
225
|
+
ev3_id = ev3["node_id"]
|
|
226
|
+
# self 层事件节点(层闸对 designer 放行 → 单独钉住保护闸)
|
|
227
|
+
self_ev = designer.add(
|
|
228
|
+
"selfevent", "# 事件正文:self 层洞见事件\n",
|
|
229
|
+
layer="self", tags=[insight.TAG_EVENT], insight_state="pending")
|
|
230
|
+
check("0b 夹具就位(三个 contextual 事件 + self 层事件;层位可查)",
|
|
231
|
+
bool(ev_id) and bool(ev2_id) and bool(ev3_id) and bool(self_ev)
|
|
232
|
+
and len({ev_id, ev2_id, ev3_id}) == 3
|
|
233
|
+
and all((designer.index["nodes"].get(x) or {}).get("layer") == "contextual"
|
|
234
|
+
for x in (ev_id, ev2_id, ev3_id))
|
|
235
|
+
and (designer.index["nodes"].get(self_ev) or {}).get("layer") == "self")
|
|
236
|
+
# knowledge 层节点:无 `# 功能名` 行(注入行会成为**首个**功能名行 → 读面顶替)
|
|
237
|
+
knode = designer.add("kn", "# 正文:知识节点占位。\n", layer="knowledge",
|
|
238
|
+
condition_space=SLOTS)
|
|
239
|
+
snode = designer.add("selfcard", "# 正文:自我状态卡占位。\n", layer="self",
|
|
240
|
+
condition_space=SLOTS, importance=0.9)
|
|
241
|
+
|
|
242
|
+
# ---------- ① insight.verify:越层 / 保护闸 ----------
|
|
243
|
+
print("[1] insight.verify:既得写面(越层腿 + 保护腿 + 反向腿)")
|
|
244
|
+
sustain = _secured(_princ("sustain"))
|
|
245
|
+
t0 = time.time()
|
|
246
|
+
ok, exc_name, out = _raises(sustain.insight, action="verify",
|
|
247
|
+
node_id=ev_id, verdict="verified")
|
|
248
|
+
check("1a sustain(layers=('self',))越层 verify 被拒(修前 = 越层改写成功)",
|
|
249
|
+
(not ok) and exc_name == "AccessDenied",
|
|
250
|
+
"exc=%s out=%s" % (exc_name, str(out)[:80]), live=True)
|
|
251
|
+
fm1, c1 = _fm_of(sustain, ev_id)
|
|
252
|
+
check("1b 被拒后盘面零改写(仍 pending、未自授保护位/0.9/基底 data)",
|
|
253
|
+
fm1.get("insight_state") == "pending" and not fm1.get("protected")
|
|
254
|
+
and float(fm1.get("importance") or 0) < 0.9
|
|
255
|
+
and fm1.get("verification_basis") != "data",
|
|
256
|
+
"state=%s protected=%s imp=%s basis=%s"
|
|
257
|
+
% (fm1.get("insight_state"), fm1.get("protected"),
|
|
258
|
+
fm1.get("importance"), fm1.get("verification_basis")), live=True)
|
|
259
|
+
check("1c 被拒路径不留伪造快照/审计(_protected_history 无该节点、审计无该条)",
|
|
260
|
+
not os.path.exists(os.path.join(ROOT, protect.HISTORY_DIR, ev_id)),
|
|
261
|
+
"history=%s" % protect.history(sustain, ev_id))
|
|
262
|
+
|
|
263
|
+
ok, exc_name, out = _raises(designer.insight, action="verify",
|
|
264
|
+
node_id=self_ev, verdict="verified")
|
|
265
|
+
check("1d designer(layers=('*',) 层闸放行)对 self 层事件覆写仍被保护闸拒"
|
|
266
|
+
"(修前 = 无痕覆写 self 层节点)",
|
|
267
|
+
(not ok) and exc_name in ("ProtectionError", "PermissionError"),
|
|
268
|
+
"exc=%s out=%s" % (exc_name, str(out)[:80]), live=True)
|
|
269
|
+
_, self_ev_content = _fm_of(designer, self_ev)
|
|
270
|
+
check("1e self 层节点正文未被无痕覆写(保护闸拒绝无副作用)",
|
|
271
|
+
self_ev_content == "# 事件正文:self 层洞见事件\n",
|
|
272
|
+
repr(self_ev_content[:60]), live=True)
|
|
273
|
+
|
|
274
|
+
rec = _secured(_princ("record"))
|
|
275
|
+
ok_r, exc_r, out_r = _raises(rec.insight, action="verify", node_id=ev_id,
|
|
276
|
+
verdict="verified",
|
|
277
|
+
evidence=[{"type": "v3", "ref": "外部确证-1"}])
|
|
278
|
+
fm_r, _c = _fm_of(rec, ev_id)
|
|
279
|
+
check("1f 反向腿:record(layers 含 contextual)同层正当 verify 仍成功",
|
|
280
|
+
ok_r and out_r.get("state") == "verified"
|
|
281
|
+
and float(out_r.get("importance") or 0) >= 0.9,
|
|
282
|
+
"ok=%s exc=%s out=%s" % (ok_r, exc_r, str(out_r)[:80]), live=True)
|
|
283
|
+
check("1g 反向腿落盘:verified 后基底升 data 且带 verified 标签",
|
|
284
|
+
fm_r.get("verification_basis") == "data"
|
|
285
|
+
and insight.TAG_VERIFIED in (fm_r.get("tags") or []),
|
|
286
|
+
"basis=%s tags=%s" % (fm_r.get("verification_basis"),
|
|
287
|
+
fm_r.get("tags")), live=True)
|
|
288
|
+
reflect = _secured(_princ("reflect"))
|
|
289
|
+
ok_rf, exc_rf, out_rf = _raises(reflect.insight, action="verify",
|
|
290
|
+
node_id=ev2_id, v_types=["v3"])
|
|
291
|
+
check("1h 反向腿:reflect(layers=('contextual',))同层 verify 仍成功",
|
|
292
|
+
ok_rf and getattr(out_rf, "get", lambda *a: None)("state") == "verified",
|
|
293
|
+
"exc=%s out=%s" % (exc_rf, str(out_rf)[:80]), live=True)
|
|
294
|
+
|
|
295
|
+
# ---------- ② ccgc.recalibrate ----------
|
|
296
|
+
print("[2] ccgc.recalibrate:层闸 / 保护闸 / 换行注入 / 反向腿")
|
|
297
|
+
before_k = _snapshot_state(designer, knode)
|
|
298
|
+
ok2, exc2, out2 = _raises(ccgc.recalibrate, snode,
|
|
299
|
+
{"observation_tool": "工具B"}, "reviewer-B",
|
|
300
|
+
"agent-A", cg=rec, apply=True)
|
|
301
|
+
check("2a record 身份改写 self 层节点被层闸拒(修前 ok=True written=1)",
|
|
302
|
+
(not ok2) and exc2 == "AccessDenied",
|
|
303
|
+
"exc=%s out=%s" % (exc2, str(out2)[:80]), live=True)
|
|
304
|
+
fm_s, c_s = _fm_of(rec, snode)
|
|
305
|
+
check("2b 被拒后 self 层节点正文与 condition_space 零改写",
|
|
306
|
+
c_s == "# 正文:自我状态卡占位。\n"
|
|
307
|
+
and fm_s.get("condition_space") == SLOTS,
|
|
308
|
+
"content=%r slots=%s" % (c_s[:40], fm_s.get("condition_space")), live=True)
|
|
309
|
+
|
|
310
|
+
ok3, exc3, out3 = _raises(ccgc.recalibrate, snode,
|
|
311
|
+
{"observation_tool": "工具B"}, "reviewer-B",
|
|
312
|
+
"agent-A", cg=designer, apply=True)
|
|
313
|
+
check("2c designer(层闸放行)改写 self 层 immutable 节点被保护闸拒"
|
|
314
|
+
"(修前 = 无痕覆写不可篡改层)",
|
|
315
|
+
(not ok3) and exc3 in ("ProtectionError", "PermissionError"),
|
|
316
|
+
"exc=%s out=%s" % (exc3, str(out3)[:80]), live=True)
|
|
317
|
+
|
|
318
|
+
ok4, exc4, out4 = _raises(ccgc.recalibrate, knode,
|
|
319
|
+
{"observation_tool": INJECT}, "reviewer-B",
|
|
320
|
+
"agent-A", cg=rec, apply=True)
|
|
321
|
+
fm_k, c_k = _fm_of(rec, knode)
|
|
322
|
+
forged = [ln for ln in c_k.split("\n") if ln.strip().startswith("# 功能名")]
|
|
323
|
+
check("2d 换行注入被拒(E022;修前 = 注入行落盘)",
|
|
324
|
+
(not ok4 and "E022" in str(out4))
|
|
325
|
+
or (ok4 and getattr(out4, "ok", False) is False
|
|
326
|
+
and any("E022" in e for e in (getattr(out4, "errors", None) or []))),
|
|
327
|
+
"exc=%s out=%s" % (exc4, str(out4)[:100]), live=True)
|
|
328
|
+
check("2e 注入被拒后盘面零改写、无伪造 `# 功能名` 独立行",
|
|
329
|
+
not forged and fm_k.get("condition_space") == SLOTS
|
|
330
|
+
and c_k == before_k[1],
|
|
331
|
+
"forged=%s slots=%s" % (forged, fm_k.get("condition_space")), live=True)
|
|
332
|
+
check("2f 读面未被顶替:ccg_field_value 未取到注入值",
|
|
333
|
+
"注入的新功能名" not in str(nodefile.ccg_field_value(c_k, "功能名")),
|
|
334
|
+
"got=%r" % nodefile.ccg_field_value(c_k, "功能名"), live=True)
|
|
335
|
+
ok5, exc5, out5 = _raises(ccgc.recalibrate, knode,
|
|
336
|
+
{"observation_tool": "改用扩展名映射表路由"},
|
|
337
|
+
"reviewer-B", "agent-A", cg=rec, apply=True)
|
|
338
|
+
fm_k2, c_k2 = _fm_of(rec, knode)
|
|
339
|
+
check("2g 反向腿:合法单行四槽修正仍落盘(written=1 + 正文生效条件行更新)",
|
|
340
|
+
ok5 and getattr(out5, "ok", False) and getattr(out5, "written", 0) == 1
|
|
341
|
+
and fm_k2.get("condition_space", {}).get("observation_tool")
|
|
342
|
+
== "改用扩展名映射表路由",
|
|
343
|
+
"exc=%s out=%s" % (exc5, str(getattr(out5, "errors", ""))[:80]), live=True)
|
|
344
|
+
check("2h 直调 _upsert_ccg_line 的换行值被拒(同族单点,非仅调用方自觉)",
|
|
345
|
+
_not_raises_but_no_forged_line(), "见 _not_raises_but_no_forged_line",
|
|
346
|
+
live=True)
|
|
347
|
+
|
|
348
|
+
# ---------- ③ ccgc.link 同款注入面 ----------
|
|
349
|
+
print("[3] ccgc.link:同款注入面 + 反向腿")
|
|
350
|
+
tk = tokens.issue("verify", actor="n197-reviewer", path=tokens.token_file())
|
|
351
|
+
comp_bad = ccgc.compile_dialog(
|
|
352
|
+
"探针对话:使用扩展名映射表进行路由。", knode, "n197-record",
|
|
353
|
+
marks={"功能名": "探针对话\n# 功能名:注入的新功能名"},
|
|
354
|
+
slots=dict(SLOTS), cg=rec, strict_spans=False)
|
|
355
|
+
check("3a 含换行的候选在**编译期**即遭 E022(阻断点前移到候选入口)",
|
|
356
|
+
not comp_bad.success
|
|
357
|
+
and any("E022" in e for e in comp_bad.errors),
|
|
358
|
+
str(comp_bad.errors)[:120], live=True)
|
|
359
|
+
at = ccgc.attest(knode, ccgc.ACCEPT, "n197-reviewer", "n197-record", cg=rec,
|
|
360
|
+
verifier_token=tk["token"])
|
|
361
|
+
lk_bad = ccgc.link(comp_bad, at, cg=rec, apply=True)
|
|
362
|
+
fm_l, c_l = _fm_of(rec, knode)
|
|
363
|
+
check("3b 编译不通过 → link 拒写、盘面无伪造行",
|
|
364
|
+
(not lk_bad.ok) and c_l == c_k2
|
|
365
|
+
and not [ln for ln in c_l.split("\n")
|
|
366
|
+
if ln.strip().startswith("# 功能名")],
|
|
367
|
+
"errs=%s" % str(lk_bad.errors)[:100], live=True)
|
|
368
|
+
comp_ok = ccgc.compile_dialog(
|
|
369
|
+
"探针对话:使用扩展名映射表进行路由。", knode, "n197-record",
|
|
370
|
+
marks={"功能名": "探针功能名", "子功能": "编译复核"},
|
|
371
|
+
slots=dict(SLOTS), cg=rec, strict_spans=False)
|
|
372
|
+
at_ok = ccgc.attest(knode, ccgc.ACCEPT, "n197-reviewer", "n197-record",
|
|
373
|
+
cg=rec, verifier_token=tk["token"])
|
|
374
|
+
lk_ok = ccgc.link(comp_ok, at_ok, cg=rec, apply=True)
|
|
375
|
+
fm_l2, c_l2 = _fm_of(rec, knode)
|
|
376
|
+
check("3c 反向腿:合法候选 + 令牌签章仍落库(link ok,六要素行写入)",
|
|
377
|
+
lk_ok.ok and getattr(lk_ok, "written", 0) >= 1
|
|
378
|
+
and "# 功能名:探针功能名" in c_l2,
|
|
379
|
+
"ok=%s errs=%s" % (lk_ok.ok, str(lk_ok.errors)[:80]), live=True)
|
|
380
|
+
|
|
381
|
+
# ---------- ④ 工具面真身 ----------
|
|
382
|
+
print("[4] 工具面真身(mcp_server.call_tool):与缺陷材料同口径")
|
|
383
|
+
ok6, exc6, out6 = _raises(mcp_server.call_tool, sustain, "cg",
|
|
384
|
+
{"op": "insight", "action": "verify",
|
|
385
|
+
"node_id": ev3_id, "verdict": "verified"})
|
|
386
|
+
fm6, c6 = _fm_of(sustain, ev3_id)
|
|
387
|
+
check("4a call_tool(op=insight, action=verify) 越层 leg 走真身同样被拒"
|
|
388
|
+
"(材料口径:sustain 对他人 contextual 事件零证据自证)",
|
|
389
|
+
(not ok6) and exc6 == "AccessDenied"
|
|
390
|
+
and fm6.get("insight_state") == "pending"
|
|
391
|
+
and not fm6.get("protected"),
|
|
392
|
+
"exc=%s state=%s protected=%s"
|
|
393
|
+
% (exc6, fm6.get("insight_state"), fm6.get("protected")), live=True)
|
|
394
|
+
ok7, exc7, out7 = _raises(mcp_server.call_tool, rec, "cg",
|
|
395
|
+
{"op": "ccg", "action": "recalibrate",
|
|
396
|
+
"ccg": {"node_id": snode, "apply": True,
|
|
397
|
+
"slot_corrections": {"observation_tool":
|
|
398
|
+
"工具B"},
|
|
399
|
+
"verifier": "reviewer-B",
|
|
400
|
+
"compiled_by": "agent-A"}})
|
|
401
|
+
fm_s4, c_s4 = _fm_of(rec, snode)
|
|
402
|
+
check("4b call_tool(op=ccg, action=recalibrate) 越层被拒且盘面零改写",
|
|
403
|
+
(not ok7) and exc7 == "AccessDenied" and c_s4 == c_s,
|
|
404
|
+
"exc=%s out=%s content=%r" % (exc7, str(out7)[:60], c_s4[:40]), live=True)
|
|
405
|
+
|
|
406
|
+
# ---------- ⑤ 源断言 + floor ----------
|
|
407
|
+
print("[5] 源断言 + 守卫自检")
|
|
408
|
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
409
|
+
"insight.py"), encoding="utf-8", errors="replace") as fh:
|
|
410
|
+
src_i = fh.read()
|
|
411
|
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
412
|
+
"ccgc.py"), encoding="utf-8", errors="replace") as fh:
|
|
413
|
+
src_c = fh.read()
|
|
414
|
+
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
415
|
+
"protect.py"), encoding="utf-8", errors="replace") as fh:
|
|
416
|
+
src_p = fh.read()
|
|
417
|
+
win_i = _src_window(src_i, "def verify(cg, node_id=None, evidence=None",
|
|
418
|
+
"# ---------------------------------------------------------------")
|
|
419
|
+
check("5a insight.verify 写点前紧邻 guard_overwrite(注释里的 `_write_node` "
|
|
420
|
+
"字样不作数:断言代码形态相邻),且层/敏感度取节点 fm 真值",
|
|
421
|
+
"protect.guard_overwrite(cg, node_id, layer=fm.get(\"layer\"),\n"
|
|
422
|
+
" sensitivity=fm.get(\"sensitivity\"), "
|
|
423
|
+
"actor=actor)\n cg._write_node(node_id,"
|
|
424
|
+
in src_i.replace("\r\n", "\n"),
|
|
425
|
+
"win_len=%d" % len(win_i))
|
|
426
|
+
check("5b ccgc.link 与 recalibrate 两个写点前各自紧邻 guard_overwrite"
|
|
427
|
+
"(层/敏感度各取本节点 fm 真值)",
|
|
428
|
+
src_c.count("protect.guard_overwrite(") == 2
|
|
429
|
+
and ("protect.guard_overwrite(_cg, node_id, layer=fm.get(\"layer\"),"
|
|
430
|
+
in src_c.replace("\r\n", "\n"))
|
|
431
|
+
and ("protect.guard_overwrite(_cg, out.node_id, layer=fm.get(\"layer\"),"
|
|
432
|
+
in src_c.replace("\r\n", "\n"))
|
|
433
|
+
and ("actor=actor or compiled.actor)\n _cg._write_node(node_id,"
|
|
434
|
+
in src_c.replace("\r\n", "\n"))
|
|
435
|
+
and ("actor=verifier)\n _cg._write_node(out.node_id,"
|
|
436
|
+
in src_c.replace("\r\n", "\n")),
|
|
437
|
+
"count=%d" % src_c.count("protect.guard_overwrite("))
|
|
438
|
+
_gw = _src_window(src_p, "def guard_overwrite", "\n\n# ")
|
|
439
|
+
_rl = _src_window(src_p, "def require_layer(cg, node_id", "\n\n# ")
|
|
440
|
+
_rt = _src_window(src_p, "def _resolve_target", "\n\n# ")
|
|
441
|
+
check("5c protect.guard_overwrite 单点内层闸 + 保护闸 + 代际探活齐备"
|
|
442
|
+
"(N209 起解析与层闸收敛到 require_layer/_resolve_target 同一单点,"
|
|
443
|
+
"故三要素分别钉在调用方与被调用方)",
|
|
444
|
+
"def guard_overwrite" in src_p
|
|
445
|
+
and "require_layer(cg, node_id" in _gw and "guard_write(" in _gw
|
|
446
|
+
and "require_layer_write" in _rl and "_maybe_reload_index" in _rt,
|
|
447
|
+
"gw_len=%d rl_len=%d rt_len=%d" % (len(_gw), len(_rl), len(_rt)))
|
|
448
|
+
check("5d ccgc 换行闸:_upsert_ccg_line 拒收 + 编译期/recalibrate 边界 E022",
|
|
449
|
+
"E022" in src_c and "含换行" in src_c
|
|
450
|
+
and "换行" in _src_window(src_c, "def _upsert_ccg_line",
|
|
451
|
+
"def _append_jsonl"),
|
|
452
|
+
"E022=%d" % src_c.count("E022"))
|
|
453
|
+
check("5e 实弹断言数 ≥ %d(防 spawn/扫描面失效假绿)" % LIVE_FLOOR,
|
|
454
|
+
LIVE >= LIVE_FLOOR, "LIVE=%d" % LIVE)
|
|
455
|
+
|
|
456
|
+
print("\n=== N197/N208 write-face gates: %d passed, %d failed (live=%d) ==="
|
|
457
|
+
% (PASS, FAIL, LIVE))
|
|
458
|
+
if FAILS:
|
|
459
|
+
print("失败项:%s" % ";".join(FAILS))
|
|
460
|
+
return 1 if FAIL else 0
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
if __name__ == "__main__":
|
|
464
|
+
_code = 0
|
|
465
|
+
try:
|
|
466
|
+
_code = main()
|
|
467
|
+
finally:
|
|
468
|
+
_close_all()
|
|
469
|
+
if not _cleanup(_TMP):
|
|
470
|
+
print("WARN: 临时根残留未清(%s)——请人工清理" % _TMP)
|
|
471
|
+
sys.exit(_code)
|