@furongjun1999/dsh-memory 0.5.0 → 0.6.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 +572 -465
- package/docs/README.md +1 -0
- 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/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/bench_lingshu_self/bench_self.py +140 -0
- package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
- package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
- package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
- package/docs/eval//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//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
- package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
- package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v1.md +156 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v10.md +210 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v11.md +227 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v12.md +203 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v13.md +233 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v14.md +191 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v15.md +213 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v16.md +214 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v17.md +199 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_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_v2.md +156 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v3.md +152 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v4.md +128 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v5.md +114 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v6.md +192 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v7.md +187 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v8.md +207 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v9.md +203 -0
- package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +19 -3
- package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
- package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
- package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/index.js +7 -1
- package/lib/init.d.ts +88 -0
- package/lib/init.js +287 -0
- package/lib/lib/roleplay_web.js +530 -443
- package/lib/lib/token_store.d.ts +7 -1
- package/lib/lib/token_store.js +12 -3
- package/md_cg/audit.py +12 -1
- package/md_cg/backfill.py +16 -15
- package/md_cg/backfill_bucket_zh.py +35 -0
- package/md_cg/bench_e2e_judge.py +532 -0
- package/md_cg/bench_e2e_locomo_qa.py +368 -0
- package/md_cg/bench_e2e_qa.py +256 -0
- package/md_cg/branches.py +18 -2
- package/md_cg/ccgc.py +3 -2
- package/md_cg/chain.py +19 -4
- package/md_cg/consolidate.py +7 -6
- package/md_cg/crosscheck.py +4 -3
- package/md_cg/crypto.py +439 -437
- package/md_cg/datapath.py +395 -335
- package/md_cg/evidence.py +8 -3
- package/md_cg/export.py +3 -1
- package/md_cg/forgetting.py +2 -2
- package/md_cg/fsutil.py +48 -0
- package/md_cg/hotcache.py +255 -238
- package/md_cg/interop.py +161 -22
- package/md_cg/judgment_manifest.py +177 -0
- package/md_cg/links.py +655 -622
- package/md_cg/mcp_server.py +282 -31
- package/md_cg/mdcg.py +702 -126
- package/md_cg/mdcos.py +454 -68
- package/md_cg/mreview/govern.py +5 -4
- package/md_cg/postings.py +4 -2
- package/md_cg/readcache.py +76 -18
- package/md_cg/reconcile.py +228 -0
- package/md_cg/review_cli.py +215 -0
- package/md_cg/routing.py +28 -0
- package/md_cg/run_tests.py +211 -0
- package/md_cg/scrub.py +862 -852
- package/md_cg/security.py +385 -275
- package/md_cg/selfreport.py +3 -2
- package/md_cg/signer.py +565 -562
- package/md_cg/sources.py +3 -2
- package/md_cg/stg.py +6 -0
- package/md_cg/sustain.py +1168 -1138
- package/md_cg/test_access_hints.py +147 -0
- package/md_cg/test_branch_discard_tombstone.py +136 -0
- package/md_cg/test_branches.py +259 -249
- package/md_cg/test_chain_read_isolate.py +168 -0
- package/md_cg/test_datapath_device_name.py +203 -0
- package/md_cg/test_emit_negtail_cache.py +156 -0
- package/md_cg/test_en_pipeline.py +186 -166
- package/md_cg/test_govern_directread.py +421 -0
- package/md_cg/test_i32_hotcache_env_key.py +218 -0
- package/md_cg/test_identity_attribution.py +228 -147
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_index_durability.py +238 -224
- package/md_cg/test_interop.py +4 -2
- package/md_cg/test_interop_judgment.py +228 -0
- package/md_cg/test_issue39_utf8_stdio.py +273 -0
- package/md_cg/test_links_concurrent_write.py +188 -0
- package/md_cg/test_merge_upsert.py +168 -0
- package/md_cg/test_n123_derive_expiry_chain.py +205 -0
- package/md_cg/test_n130_verify_falsified_protect.py +185 -0
- package/md_cg/test_n131_merge_gate.py +205 -0
- package/md_cg/test_none_id_write_guard.py +165 -0
- package/md_cg/test_p1x_ref_root.py +160 -0
- package/md_cg/test_p27_docindex.py +774 -765
- package/md_cg/test_p2_mcp.py +3 -0
- package/md_cg/test_p32_backfill.py +304 -298
- package/md_cg/test_p39_verify_flow.py +90 -50
- package/md_cg/test_p47_session_view.py +60 -25
- package/md_cg/test_propose_tail_index.py +157 -0
- package/md_cg/test_read_scope_b27.py +277 -0
- package/md_cg/test_readcache_default_on.py +168 -0
- package/md_cg/test_readcache_precise_inval.py +270 -0
- package/md_cg/test_readcache_prodpath.py +55 -7
- package/md_cg/test_reconcile_v0.py +294 -0
- package/md_cg/test_retr_s1.py +6 -2
- package/md_cg/test_retr_s1b.py +67 -0
- package/md_cg/test_retr_s7.py +8 -0
- package/md_cg/test_retr_s9_entity_ctx.py +181 -175
- package/md_cg/test_retr_score_once.py +208 -0
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_onepass.py +170 -0
- package/md_cg/test_rrf_graph_seed_cache.py +154 -0
- package/md_cg/test_security_audit.py +155 -0
- package/md_cg/test_security_audit_b26.py +161 -0
- package/md_cg/test_security_audit_v21.py +250 -0
- package/md_cg/test_semantic_canonical.py +255 -241
- package/md_cg/test_session_isolation.py +168 -0
- package/md_cg/test_snapshot_autoclose.py +187 -0
- package/md_cg/test_tail_watermark_race.py +208 -0
- package/md_cg/test_tenant_env_override_warn.py +139 -0
- package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
- package/md_cg/test_v14_fixes.py +415 -397
- package/md_cg/test_verify_dirty_reconcile.py +157 -0
- package/md_cg/theory.py +276 -273
- package/md_cg/tokens.py +734 -677
- package/md_cg/units.py +3 -2
- package/md_cg/vision_evidence.py +4 -3
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
- package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
- package/md_cg/writepipe.py +554 -550
- package/package.json +11 -2
- package/src/bridge.ts +434 -401
- package/src/cli.ts +65 -0
- package/src/index.ts +526 -518
- package/src/init.ts +347 -0
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""连接层并发写守卫(缺陷:load→改内存→save 无锁 + 固定临时名互踩)。
|
|
3
|
+
|
|
4
|
+
缺陷形态(缺陷报告 2026-09-25 复现证实,medium):links 连接层全部变更
|
|
5
|
+
(handshake/observe/promote/degrade/isolate/withdraw/decay)是
|
|
6
|
+
load→改内存→save 的读-改-写,无跨进程锁;save 用固定临时名 p+".tmp"
|
|
7
|
+
(fsutil.py:6-8 明载的已实名反模式:两写者共享同一临时文件,后者截断
|
|
8
|
+
前者写了一半的内容)。并发下后写者整文件覆盖前写者——静默丢一方信任
|
|
9
|
+
更新(20/20 实证:终态一方 evidence_count=0),互踩 .tmp 产生交织内容
|
|
10
|
+
(audit 超单写者上限实拍)。
|
|
11
|
+
|
|
12
|
+
修复方向:save 改走 fsutil.atomic_write(mkstemp 唯一临时名 + 带短重试
|
|
13
|
+
rename);各变更原语以 FileLock 为 load→mutate→save 全程加连接级互斥。
|
|
14
|
+
|
|
15
|
+
红守卫 = 缺陷报告 Barrier 对齐双线程复现(两线程都完成 load+mutate 后
|
|
16
|
+
在 save 点对齐同时落盘):
|
|
17
|
+
S1 observe×observe(不同连接):双方都返回 ok 而终态一方 evidence_count
|
|
18
|
+
丢失 = 静默丢更新(红);audit 超上限/文件不可解析亦不得出现;
|
|
19
|
+
S2 observe×degrade(跨原语家族):同一读-改-写形态,两方效果都幸存。
|
|
20
|
+
对齐窗处理:barrier 超时(3s)短于 FileLock 超时(10s)——修复后另一
|
|
21
|
+
写者被连接锁挡在临界区外、barrier 自然超时Broken,捕获后照常落盘(无
|
|
22
|
+
死锁);修复前双方都快速到达 save 点、barrier 正常放行 → 确定性竞态。
|
|
23
|
+
运行:python -m md_cg.test_links_concurrent_write
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
import threading
|
|
32
|
+
|
|
33
|
+
from . import links
|
|
34
|
+
|
|
35
|
+
PASS = FAIL = 0
|
|
36
|
+
FAILS = []
|
|
37
|
+
|
|
38
|
+
BARRIER_TIMEOUT = 3.0 # < FileLock 默认 10s:锁化后对齐窗自然超时,不死锁
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ok(cond, label):
|
|
42
|
+
global PASS, FAIL
|
|
43
|
+
if cond:
|
|
44
|
+
PASS += 1
|
|
45
|
+
print(f" OK {label}")
|
|
46
|
+
else:
|
|
47
|
+
FAIL += 1
|
|
48
|
+
FAILS.append(label)
|
|
49
|
+
print(f" FAIL {label}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _race(p, calls):
|
|
53
|
+
"""双线程在 save 点 Barrier 对齐并发执行 calls 里的两个变更。
|
|
54
|
+
|
|
55
|
+
calls: [(fn, kwargs), (fn, kwargs)]——fn 为 links 模块变更原语。
|
|
56
|
+
返回 (两线程结果, 对齐是否达成)。对齐未达成(barrier 超时 Broken)
|
|
57
|
+
属修复后预期形态,单独如实报告。
|
|
58
|
+
"""
|
|
59
|
+
real_save = links.save
|
|
60
|
+
barrier = threading.Barrier(2)
|
|
61
|
+
aligned = {"hit": True}
|
|
62
|
+
results = [None, None]
|
|
63
|
+
|
|
64
|
+
def aligned_save(data, path=None):
|
|
65
|
+
try:
|
|
66
|
+
barrier.wait(timeout=BARRIER_TIMEOUT)
|
|
67
|
+
except threading.BrokenBarrierError:
|
|
68
|
+
aligned["hit"] = False # 另一写者未到(被锁挡住)——修复后预期
|
|
69
|
+
return real_save(data, path)
|
|
70
|
+
|
|
71
|
+
def worker(i, fn, kw):
|
|
72
|
+
try:
|
|
73
|
+
results[i] = {"ok": fn(**kw).get("ok")}
|
|
74
|
+
except Exception as exc: # noqa: BLE001
|
|
75
|
+
results[i] = {"exc": f"{type(exc).__name__}: {exc}"}
|
|
76
|
+
|
|
77
|
+
links.save = aligned_save
|
|
78
|
+
try:
|
|
79
|
+
ts = [threading.Thread(target=worker, args=(i, fn, kw))
|
|
80
|
+
for i, (fn, kw) in enumerate(calls)]
|
|
81
|
+
for t in ts:
|
|
82
|
+
t.start()
|
|
83
|
+
for t in ts:
|
|
84
|
+
t.join(timeout=60)
|
|
85
|
+
finally:
|
|
86
|
+
links.save = real_save
|
|
87
|
+
return results, aligned["hit"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _seq_ref_audit_len(tmp):
|
|
91
|
+
"""单写者合法基线:同序列(handshake→observe)在无并发下顺序重放的
|
|
92
|
+
audit 长度(本机实测 3:handshake + version_misaligned + observe——
|
|
93
|
+
未声明版本会多记一条,故不得硬编码 2)。"""
|
|
94
|
+
p = os.path.join(tmp, "ref", "_links.json")
|
|
95
|
+
links.handshake("peerRef", path=p)
|
|
96
|
+
links.observe("agent:peerRef", evidence="顺序基线", positive=True, path=p)
|
|
97
|
+
return len(links.get("agent:peerRef", path=p)["audit"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _get_link(p, peer):
|
|
101
|
+
data = links.load(p)
|
|
102
|
+
for lk in (data.get("links") or {}).values():
|
|
103
|
+
if lk.get("peer_node_id") == "agent:" + peer:
|
|
104
|
+
return lk
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _scenario_observe_race(tmp):
|
|
109
|
+
"""S1:双线程并发 observe 不同连接——对齐在 save 点。"""
|
|
110
|
+
print("== S1 observe×observe(save 点对齐) ==")
|
|
111
|
+
p = os.path.join(tmp, "s1", "_links.json")
|
|
112
|
+
links.handshake("peerA", path=p)
|
|
113
|
+
links.handshake("peerB", path=p)
|
|
114
|
+
|
|
115
|
+
results, aligned = _race(p, [
|
|
116
|
+
(links.observe, {"peer": "agent:peerA", "evidence": "并发证据A",
|
|
117
|
+
"positive": True, "path": p}),
|
|
118
|
+
(links.observe, {"peer": "agent:peerB", "evidence": "并发证据B",
|
|
119
|
+
"positive": True, "path": p}),
|
|
120
|
+
])
|
|
121
|
+
ok(all(r and r.get("ok") for r in results),
|
|
122
|
+
f"S1a 双方 observe 均成功无异常(实测 {results})——静默丢更新的前提"
|
|
123
|
+
f"是『返回 ok 但更新消失』")
|
|
124
|
+
print(f" .. save 点对齐达成={aligned}(修复前应 True=真竞态;"
|
|
125
|
+
f"修复后 False=另一写者被锁挡住,属预期)")
|
|
126
|
+
|
|
127
|
+
la, lb = _get_link(p, "peerA"), _get_link(p, "peerB")
|
|
128
|
+
ok(la is not None and lb is not None,
|
|
129
|
+
"S1b 终态文件可解析且两条连接都在(无交织撕裂丢连接)")
|
|
130
|
+
eca = (la or {}).get("evidence_count")
|
|
131
|
+
ecb = (lb or {}).get("evidence_count")
|
|
132
|
+
ok(eca == 1 and ecb == 1,
|
|
133
|
+
f"S1c 无静默丢更新(peerA={eca}/peerB={ecb},应 1/1——旧代码"
|
|
134
|
+
f"后写者整文件覆盖前写者,一方恒 0)")
|
|
135
|
+
ref = _seq_ref_audit_len(tmp)
|
|
136
|
+
aa = len((la or {}).get("audit") or [])
|
|
137
|
+
ab = len((lb or {}).get("audit") or [])
|
|
138
|
+
ok(aa == ref and ab == ref,
|
|
139
|
+
f"S1d audit 恰为单写者合法基线(A={aa}/B={ab},顺序重放基线={ref};"
|
|
140
|
+
f"旧代码丢更新侧偏少、固定临时名互踩可超限——缺陷实拍 audit 超上限)")
|
|
141
|
+
ok(not os.path.exists(p + ".tmp"),
|
|
142
|
+
"S1e 固定临时名不再出现(改走 atomic_write 唯一临时名)")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _scenario_cross_family(tmp):
|
|
146
|
+
"""S2:observe×degrade 跨原语家族(同一 load→mutate→save 形态)。"""
|
|
147
|
+
print("== S2 observe×degrade(跨原语) ==")
|
|
148
|
+
p = os.path.join(tmp, "s2", "_links.json")
|
|
149
|
+
links.handshake("peerC", path=p)
|
|
150
|
+
links.handshake("peerD", path=p)
|
|
151
|
+
|
|
152
|
+
results, aligned = _race(p, [
|
|
153
|
+
(links.observe, {"peer": "agent:peerC", "evidence": "并发证据C",
|
|
154
|
+
"positive": True, "path": p}),
|
|
155
|
+
(links.degrade, {"peer": "agent:peerD", "reason": "并发降级守卫",
|
|
156
|
+
"path": p}),
|
|
157
|
+
])
|
|
158
|
+
ok(all(r and r.get("ok") for r in results),
|
|
159
|
+
f"S2a 双方均成功无异常(实测 {results})")
|
|
160
|
+
print(f" .. save 点对齐达成={aligned}")
|
|
161
|
+
lc, ld = _get_link(p, "peerC"), _get_link(p, "peerD")
|
|
162
|
+
ok(lc is not None and ld is not None, "S2b 两条连接都在(文件未撕裂)")
|
|
163
|
+
ok((lc or {}).get("evidence_count") == 1
|
|
164
|
+
and (ld or {}).get("status") == "degraded",
|
|
165
|
+
f"S2c 两方效果都幸存(C.evidence_count="
|
|
166
|
+
f"{(lc or {}).get('evidence_count')},D.status="
|
|
167
|
+
f"{(ld or {}).get('status')}——旧代码丢一方)")
|
|
168
|
+
ok(not os.path.exists(p + ".tmp"), "S2d 固定临时名不再出现")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main():
|
|
172
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_links_race_")
|
|
173
|
+
try:
|
|
174
|
+
_scenario_observe_race(tmp)
|
|
175
|
+
_scenario_cross_family(tmp)
|
|
176
|
+
finally:
|
|
177
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
178
|
+
|
|
179
|
+
print("\n" + "=" * 60)
|
|
180
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
181
|
+
if FAILS:
|
|
182
|
+
for f in FAILS:
|
|
183
|
+
print(f" - {f}")
|
|
184
|
+
return 0 if FAIL == 0 else 1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""merge 裁决定向增量收尾守卫(issue:merge 用 rebuild_index O(N) 收尾)。
|
|
4
|
+
|
|
5
|
+
缺陷形态(缺陷报告实测):review_decide(merge) 只改一个目标节点
|
|
6
|
+
(content 追加 + 不适用条件并集)却用 rebuild_index 收尾——_scan_nodes
|
|
7
|
+
全库逐文件 open+parse(1500 池实测 ~97-247ms/次),而同场景 accept/edit
|
|
8
|
+
走 add+flush 增量收尾(O(1),~3.4ms)。
|
|
9
|
+
|
|
10
|
+
本守卫断言(调用计数/对账形态,非时间阈值):
|
|
11
|
+
M1 merge 零全库扫描:merge 裁决期间 _scan_nodes 调用 == 0
|
|
12
|
+
(旧代码 rebuild→1 次全库扫描 → 红);
|
|
13
|
+
M2 upsert 与 rebuild 同源零漂移:merge 完成后目标节点索引条目与
|
|
14
|
+
之后再 rebuild_index 的条目逐位相等(entry 构造唯一真源);
|
|
15
|
+
M3 merge 语义保持:content 追加命中、不适用条件并集、裁决记录
|
|
16
|
+
accepted、级联关闭同内容兄弟提案;
|
|
17
|
+
M4 accept 对照不受影响(零全库扫描、结果 ok);
|
|
18
|
+
M5 定向收尾落增量日志:merge 后未 flush 尾部(_dirty)在统一收尾
|
|
19
|
+
flush 后,新进程重开可见目标新条目(增量账本,不靠 rebuild 救回)。
|
|
20
|
+
"""
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
27
|
+
|
|
28
|
+
import md_cg.mdcg as mdcg_mod # noqa: E402
|
|
29
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
30
|
+
|
|
31
|
+
PASS = 0
|
|
32
|
+
FAIL = 0
|
|
33
|
+
FAILS = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check(name, cond, detail=""):
|
|
37
|
+
global PASS, FAIL
|
|
38
|
+
if cond:
|
|
39
|
+
PASS += 1
|
|
40
|
+
print(f" OK {name}")
|
|
41
|
+
else:
|
|
42
|
+
FAIL += 1
|
|
43
|
+
FAILS.append(name)
|
|
44
|
+
print(f" FAIL {name} {detail}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def count_scans(fn):
|
|
48
|
+
n = 0
|
|
49
|
+
real = mdcg_mod.MdCG._scan_nodes
|
|
50
|
+
|
|
51
|
+
def _spy(self):
|
|
52
|
+
nonlocal n
|
|
53
|
+
n += 1
|
|
54
|
+
return real(self)
|
|
55
|
+
|
|
56
|
+
mdcg_mod.MdCG._scan_nodes = _spy
|
|
57
|
+
try:
|
|
58
|
+
return fn(), n
|
|
59
|
+
finally:
|
|
60
|
+
mdcg_mod.MdCG._scan_nodes = real
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build(root, n=6):
|
|
64
|
+
cg = MdCGOS(root)
|
|
65
|
+
for i in range(n):
|
|
66
|
+
cg.add("mem_%02d" % i, "# 功能名:样本 %d\n# 正文:蜂群调度 内容 %d" % (i, i),
|
|
67
|
+
layer="knowledge")
|
|
68
|
+
cg.add("tgt", "# 功能名:合并目标\n# 正文:目标原始内容 星云\n目标正文。",
|
|
69
|
+
layer="knowledge", tags=["合并"],
|
|
70
|
+
non_applicable_conditions=["旧条件甲"])
|
|
71
|
+
cg.close()
|
|
72
|
+
return root
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main():
|
|
76
|
+
root = tempfile.mkdtemp(prefix="mdcg_merge_guard_")
|
|
77
|
+
try:
|
|
78
|
+
build(root)
|
|
79
|
+
cg = MdCGOS(root)
|
|
80
|
+
pid = cg.propose("cand", "# 功能名:候选\n# 正文:候选正文 水位信箱",
|
|
81
|
+
layer="knowledge")
|
|
82
|
+
# 同内容兄弟提案(级联关闭面):propose 对同 payload_hash 幂等返回
|
|
83
|
+
# 既有 pid——兄弟须以他进程形态手工入队(同 phash 不同 pid)
|
|
84
|
+
from md_cg.fsutil import append_jsonl
|
|
85
|
+
from md_cg.mdcos import _sig
|
|
86
|
+
sib_content = "# 功能名:候选\n# 正文:候选正文 水位信箱"
|
|
87
|
+
append_jsonl(cg.inbox_log,
|
|
88
|
+
{"t": 1.0, "pid": "prop_sib_manual", "id": "cand_sib",
|
|
89
|
+
"content": sib_content, "layer": "knowledge",
|
|
90
|
+
"tags": [], "condition_space": {}, "verify": {},
|
|
91
|
+
"payload_hash": _sig(sib_content), "actor": "ext"})
|
|
92
|
+
|
|
93
|
+
out, n_scan = count_scans(
|
|
94
|
+
lambda: cg.review_decide(pid, "merge", merge_into="tgt",
|
|
95
|
+
reason="guard"))
|
|
96
|
+
print(" (merge 期间 _scan_nodes 调用 %d 次)" % n_scan)
|
|
97
|
+
check("M1a merge 裁决零全库扫描(旧代码 rebuild→1 次 O(N) 扫描)",
|
|
98
|
+
n_scan == 0, f"scans={n_scan}")
|
|
99
|
+
check("M3a merge 返回 ok 且 node_id 指向目标",
|
|
100
|
+
out.get("ok") and out.get("node_id") == "tgt", str(out)[:120])
|
|
101
|
+
|
|
102
|
+
# M2 upsert 与 rebuild 同源:先存 merge 后条目,rebuild 后逐位比对
|
|
103
|
+
e_after_merge = dict(cg.index["nodes"].get("tgt") or {})
|
|
104
|
+
cg.rebuild_index()
|
|
105
|
+
e_after_rebuild = dict(cg.index["nodes"].get("tgt") or {})
|
|
106
|
+
check("M2a merge 定向 upsert 条目与 rebuild 后逐位一致(同源零漂移)",
|
|
107
|
+
e_after_merge == e_after_rebuild,
|
|
108
|
+
f"diff={ {k: (e_after_merge.get(k), e_after_rebuild.get(k)) for k in set(e_after_merge) | set(e_after_rebuild) if e_after_merge.get(k) != e_after_rebuild.get(k)} }")
|
|
109
|
+
|
|
110
|
+
node = cg.get("tgt")
|
|
111
|
+
check("M3b content 追加命中(候选正文并入目标)",
|
|
112
|
+
node and "候选正文 水位信箱" in (node.get("content") or ""),
|
|
113
|
+
str(node)[:80])
|
|
114
|
+
neg = (node.get("frontmatter") or {}).get("non_applicable_conditions")
|
|
115
|
+
check("M3c 不适用条件并集(fm 旧条件保留,无新增时原样)",
|
|
116
|
+
neg == ["旧条件甲"], f"neg={neg}")
|
|
117
|
+
|
|
118
|
+
st = cg._pid_status().get(pid) or {}
|
|
119
|
+
check("M3d 裁决记录 accepted", st.get("status") == "accepted",
|
|
120
|
+
str(st)[:80])
|
|
121
|
+
casc = out.get("cascade_closed") or []
|
|
122
|
+
check("M3e 级联关闭同内容兄弟提案(手工入队形态)",
|
|
123
|
+
"prop_sib_manual" in casc, f"casc={casc}")
|
|
124
|
+
|
|
125
|
+
# M4 accept 对照
|
|
126
|
+
print("== M4 accept 对照 ==")
|
|
127
|
+
root4 = tempfile.mkdtemp(prefix="mdcg_merge_guard4_")
|
|
128
|
+
try:
|
|
129
|
+
build(root4)
|
|
130
|
+
cg4 = MdCGOS(root4)
|
|
131
|
+
pid4 = cg4.propose("acc", "# 功能名:接受候选\n# 正文:接受正文",
|
|
132
|
+
layer="knowledge")
|
|
133
|
+
out4, n_scan4 = count_scans(
|
|
134
|
+
lambda: cg4.review_decide(pid4, "accept", reason="guard"))
|
|
135
|
+
check("M4a accept 裁决零全库扫描(对照不受影响)",
|
|
136
|
+
n_scan4 == 0, f"scans={n_scan4}")
|
|
137
|
+
check("M4b accept 落节点 ok",
|
|
138
|
+
out4.get("ok") and out4.get("node_id") == "acc", str(out4)[:80])
|
|
139
|
+
cg4.close()
|
|
140
|
+
finally:
|
|
141
|
+
shutil.rmtree(root4, ignore_errors=True)
|
|
142
|
+
|
|
143
|
+
# M5 增量账本:merge 库重开(新进程形态),目标条目可见且带新内容指纹
|
|
144
|
+
print("== M5 增量账本跨进程可见 ==")
|
|
145
|
+
cg.close()
|
|
146
|
+
cg5 = MdCGOS(root)
|
|
147
|
+
try:
|
|
148
|
+
e5 = (cg5.index.get("nodes") or {}).get("tgt")
|
|
149
|
+
node5 = cg5.get("tgt")
|
|
150
|
+
check("M5a 重开后目标条目在索引(增量日志,不靠 rebuild 救回)",
|
|
151
|
+
bool(e5))
|
|
152
|
+
check("M5b 重开后目标内容为合并后版本",
|
|
153
|
+
node5 and "候选正文 水位信箱" in (node5.get("content") or ""))
|
|
154
|
+
finally:
|
|
155
|
+
cg5.close()
|
|
156
|
+
finally:
|
|
157
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
158
|
+
|
|
159
|
+
print("\n" + "=" * 60)
|
|
160
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
161
|
+
if FAILS:
|
|
162
|
+
for f in FAILS:
|
|
163
|
+
print(f" - {f}")
|
|
164
|
+
return 0 if FAIL == 0 else 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main())
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""N123 攻击复现守卫:derive 子令牌 TTL 无上限夹紧 + 过期不沿派生链传播。
|
|
3
|
+
|
|
4
|
+
缺陷(v15 留档 + 引擎域/检索域复测三度成立):
|
|
5
|
+
① derive() 子令牌 expires_at=(now+ttl) 不与父 expires_at 取 min——
|
|
6
|
+
父 ttl=0.5s、子 ttl=999999 时子比父多活 11.5 天;
|
|
7
|
+
② verify_token 只查自身记录 expires_at——父令牌过期后子令牌仍以原密级通过。
|
|
8
|
+
攻击面:持 delegable designer 令牌面(宿主进程/提示注入驱动 derive)先派生
|
|
9
|
+
长 TTL 子令牌再令父令牌自然到期——授权回收被时间维度旁路(revoke 有级联
|
|
10
|
+
tokens.py:523-526,唯过期没有,「派生只能收窄」缺时间维度)。
|
|
11
|
+
|
|
12
|
+
修复方向:子 expires_at=min(请求值, 父 expires_at)(父无界取请求值)
|
|
13
|
+
+ verify_token 沿 parent 链传播过期校验(防环;父记录缺失告警不阻断)。
|
|
14
|
+
|
|
15
|
+
运行:python -m md_cg.test_n123_derive_expiry_chain
|
|
16
|
+
纪律:全程 MDCG_TOKEN_FILE / path= 指向系统临时目录哑令牌文件,不触真实令牌库。
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
from . import tokens
|
|
28
|
+
from .tokens import TOKEN_FILE_ENV, TokenError
|
|
29
|
+
|
|
30
|
+
PASS = FAIL = 0
|
|
31
|
+
FAILS = []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check(name, cond, detail=""):
|
|
35
|
+
global PASS, FAIL
|
|
36
|
+
if cond:
|
|
37
|
+
PASS += 1
|
|
38
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
39
|
+
else:
|
|
40
|
+
FAIL += 1
|
|
41
|
+
FAILS.append(name)
|
|
42
|
+
print(f" [FAIL] {name} · {detail}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def denied(fn, *a, **kw):
|
|
46
|
+
"""执行 fn,返回 (是否被拒, 错误文本)。"""
|
|
47
|
+
try:
|
|
48
|
+
fn(*a, **kw)
|
|
49
|
+
return False, ""
|
|
50
|
+
except TokenError as e:
|
|
51
|
+
return True, str(e)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
56
|
+
root = tempfile.mkdtemp(prefix="mdcg_n123_")
|
|
57
|
+
tf = os.path.join(root, "_tokens.json")
|
|
58
|
+
# 哑环境兜底:即使代码内部遗漏 path= 也绝不回落到真实 ~/.mdcg 令牌库
|
|
59
|
+
old_env = os.environ.get(TOKEN_FILE_ENV)
|
|
60
|
+
os.environ[TOKEN_FILE_ENV] = tf
|
|
61
|
+
try:
|
|
62
|
+
# ========== 攻击复现 ①:derive 长 TTL 子令牌不被父界夹紧 ==========
|
|
63
|
+
print("\n[1] derive 夹紧:子 expires_at = min(请求值, 父 expires_at)")
|
|
64
|
+
d = tokens.issue("designer", actor="n123-designer", ttl=3600.0, path=tf)
|
|
65
|
+
child = tokens.derive(d["token"], "record", actor="n123-child",
|
|
66
|
+
ttl=999999.0, path=tf)
|
|
67
|
+
dexp = d["expires_at"]
|
|
68
|
+
cexp = child["expires_at"]
|
|
69
|
+
check("子令牌过期时间不超过父令牌", cexp is not None and dexp is not None
|
|
70
|
+
and float(cexp) <= float(dexp) + 1e-6,
|
|
71
|
+
f"child-parent={float(cexp) - float(dexp):.1f}s(旧代码 +999639s)")
|
|
72
|
+
check("夹紧开口 clamped 标注 expires_at",
|
|
73
|
+
"expires_at" in (child.get("clamped") or []),
|
|
74
|
+
f"clamped={child.get('clamped')}")
|
|
75
|
+
|
|
76
|
+
# ========== 攻击复现 ②:父过期后子令牌沿链拒验(免 sleep:手工置父已过期) ==========
|
|
77
|
+
print("\n[2] verify 沿链传播:父记录过期 → 子令牌拒绝")
|
|
78
|
+
data = json.load(open(tf, encoding="utf-8"))
|
|
79
|
+
data["tokens"][d["token_id"]]["expires_at"] = time.time() - 1.0
|
|
80
|
+
with open(tf, "w", encoding="utf-8") as f:
|
|
81
|
+
json.dump(data, f, ensure_ascii=False)
|
|
82
|
+
ok_p, why_p = denied(tokens.verify_token, d["token"], path=tf)
|
|
83
|
+
ok_c, why_c = denied(tokens.verify_token, child["token"], path=tf)
|
|
84
|
+
check("父令牌过期被拒(基线,旧代码即拦)", ok_p, why_p[:60])
|
|
85
|
+
check("父过期后子令牌沿链拒验(N123 核心断言)", ok_c, why_c[:60])
|
|
86
|
+
|
|
87
|
+
# ========== 攻击复现 ③:真实自然到期端到端 ==========
|
|
88
|
+
print("\n[3] 端到端:父 ttl=0.4 自然到期 → 长 TTL 子令牌(ttl=999999)失效")
|
|
89
|
+
d3 = tokens.issue("designer", actor="n123-e2e", ttl=0.4, path=tf)
|
|
90
|
+
c3 = tokens.derive(d3["token"], "record", actor="n123-e2e-child",
|
|
91
|
+
ttl=999999.0, path=tf)
|
|
92
|
+
check("派生时子界已被父夹紧",
|
|
93
|
+
float(c3["expires_at"]) <= float(d3["expires_at"]) + 1e-6,
|
|
94
|
+
f"child={c3['expires_at']:.1f} parent={d3['expires_at']:.1f}")
|
|
95
|
+
time.sleep(0.7)
|
|
96
|
+
ok_p3, why_p3 = denied(tokens.verify_token, d3["token"], path=tf)
|
|
97
|
+
ok_c3, why_c3 = denied(tokens.verify_token, c3["token"], path=tf)
|
|
98
|
+
check("父自然到期被拒", ok_p3, why_p3[:60])
|
|
99
|
+
check("子令牌随父到期失效(修复前:verify OK 继续用)", ok_c3, why_c3[:60])
|
|
100
|
+
|
|
101
|
+
# ========== 正向 ④:父无界时取请求值(不误夹成无界/不过度收窄) ==========
|
|
102
|
+
print("\n[4] 父无界:子 TTL 取请求值")
|
|
103
|
+
d4 = tokens.issue("designer", actor="n123-bounded", path=tf) # 无 ttl
|
|
104
|
+
check("父无界 expires_at=None", d4["expires_at"] is None)
|
|
105
|
+
c4 = tokens.derive(d4["token"], "record", actor="n123-bounded-child",
|
|
106
|
+
ttl=60.0, path=tf)
|
|
107
|
+
now = time.time()
|
|
108
|
+
check("父无界时子 expires_at=now+60(取请求值)",
|
|
109
|
+
c4["expires_at"] is not None
|
|
110
|
+
and 58.0 <= float(c4["expires_at"]) - now <= 62.0,
|
|
111
|
+
f"delta={float(c4['expires_at']) - now:.1f}s")
|
|
112
|
+
check("父无界夹紧不开口",
|
|
113
|
+
"expires_at" not in (c4.get("clamped") or []),
|
|
114
|
+
f"clamped={c4.get('clamped')}")
|
|
115
|
+
# 子短于父:取请求值不延长
|
|
116
|
+
c4b = tokens.derive(d4["token"], "record", actor="n123-short", ttl=1.0,
|
|
117
|
+
path=tf)
|
|
118
|
+
check("子短于父取请求值", c4b["expires_at"] is not None
|
|
119
|
+
and float(c4b["expires_at"]) - now <= 2.0)
|
|
120
|
+
# 子无 ttl:沿用父(旧行为保持——父无界即无界)
|
|
121
|
+
c4c = tokens.derive(d4["token"], "record", actor="n123-inherit", path=tf)
|
|
122
|
+
check("子未指定 ttl 沿用父界(无界父→无界子)",
|
|
123
|
+
c4c["expires_at"] is None, f"expires_at={c4c['expires_at']}")
|
|
124
|
+
|
|
125
|
+
# ========== 正向 ⑤:子自身先到期仍按自身拒(不因沿链放宽) ==========
|
|
126
|
+
print("\n[5] 子自身过期仍拦截")
|
|
127
|
+
d5 = tokens.issue("designer", actor="n123-self", ttl=3600.0, path=tf)
|
|
128
|
+
c5 = tokens.derive(d5["token"], "record", actor="n123-self-child",
|
|
129
|
+
ttl=0.05, path=tf)
|
|
130
|
+
time.sleep(0.15)
|
|
131
|
+
ok_c5, why_c5 = denied(tokens.verify_token, c5["token"], path=tf)
|
|
132
|
+
ok_d5, _ = denied(tokens.verify_token, d5["token"], path=tf)
|
|
133
|
+
check("子自身先到期被拒", ok_c5 and not ok_d5, why_c5[:60])
|
|
134
|
+
|
|
135
|
+
# ========== 加固 ⑥:存量库三层链 + 防环 ==========
|
|
136
|
+
print("\n[6] 存量库三层链传播 + parent 环防护")
|
|
137
|
+
# 三层链:gp(无界)→ p(已过期)→ gc;修复前 verify(gc) 只看自身 → OK
|
|
138
|
+
d6 = tokens.issue("designer", actor="n123-gp", path=tf)
|
|
139
|
+
p6 = tokens.derive(d6["token"], "orchestr", actor="n123-p", path=tf)
|
|
140
|
+
# 手工构造第三层(正常 derive 封口 delegable=False,模拟存量库/手工库)
|
|
141
|
+
data6 = json.load(open(tf, encoding="utf-8"))
|
|
142
|
+
prec = data6["tokens"][p6["token_id"]]
|
|
143
|
+
gc_id = "tk_n123gc01"
|
|
144
|
+
data6["tokens"][gc_id] = dict(prec, role="record", actor="n123-gc",
|
|
145
|
+
delegable=False, parent=p6["token_id"],
|
|
146
|
+
expires_at=time.time() + 999999.0)
|
|
147
|
+
data6["tokens"][p6["token_id"]]["expires_at"] = time.time() - 1.0
|
|
148
|
+
with open(tf, "w", encoding="utf-8") as f:
|
|
149
|
+
json.dump(data6, f, ensure_ascii=False)
|
|
150
|
+
gc_token = tokens.make_token("record", gc_id, "x" * 43)
|
|
151
|
+
# 摘要需匹配:直接改库中 hash
|
|
152
|
+
data6 = json.load(open(tf, encoding="utf-8"))
|
|
153
|
+
data6["tokens"][gc_id]["hash"] = tokens._hash("x" * 43)
|
|
154
|
+
with open(tf, "w", encoding="utf-8") as f:
|
|
155
|
+
json.dump(data6, f, ensure_ascii=False)
|
|
156
|
+
t0 = time.monotonic()
|
|
157
|
+
ok_gc, why_gc = denied(tokens.verify_token, gc_token, path=tf)
|
|
158
|
+
cost = time.monotonic() - t0
|
|
159
|
+
check("中间层过期 → 孙令牌沿链拒验", ok_gc, why_gc[:60])
|
|
160
|
+
check("沿链校验无死循环(<2s)", cost < 2.0, f"{cost:.3f}s")
|
|
161
|
+
# 防环:A.parent=B 且 B.parent=A,均未过期 → 应通过且不挂
|
|
162
|
+
data6 = json.load(open(tf, encoding="utf-8"))
|
|
163
|
+
a_id, b_id = "tk_n123loopA", "tk_n123loopB"
|
|
164
|
+
data6["tokens"][a_id] = {"role": "designer", "actor": "loop-a",
|
|
165
|
+
"tenant": "default", "clearance": "secret",
|
|
166
|
+
"can_write": True, "can_admin": True,
|
|
167
|
+
"layers_allow": ["*"], "ops_allow": ["*"],
|
|
168
|
+
"delegable": False, "parent": b_id,
|
|
169
|
+
"issued_by": "loop", "issued_at": now,
|
|
170
|
+
"expires_at": None, "revoked_at": None,
|
|
171
|
+
"label": "", "hash": tokens._hash("la" * 16)}
|
|
172
|
+
data6["tokens"][b_id] = {"role": "record", "actor": "loop-b",
|
|
173
|
+
"tenant": "default", "clearance": "internal",
|
|
174
|
+
"can_write": True, "can_admin": False,
|
|
175
|
+
"layers_allow": None, "ops_allow": None,
|
|
176
|
+
"delegable": False, "parent": a_id,
|
|
177
|
+
"issued_by": "loop", "issued_at": now,
|
|
178
|
+
"expires_at": None, "revoked_at": None,
|
|
179
|
+
"label": "", "hash": tokens._hash("lb" * 16)}
|
|
180
|
+
with open(tf, "w", encoding="utf-8") as f:
|
|
181
|
+
json.dump(data6, f, ensure_ascii=False)
|
|
182
|
+
t0 = time.monotonic()
|
|
183
|
+
try:
|
|
184
|
+
tokens.verify_token(tokens.make_token("designer", a_id, "la" * 16),
|
|
185
|
+
path=tf)
|
|
186
|
+
loop_ok = True
|
|
187
|
+
except TokenError:
|
|
188
|
+
loop_ok = True # 拒绝也可(fail-closed),关键是不挂
|
|
189
|
+
cost = time.monotonic() - t0
|
|
190
|
+
check("parent 环不挂死(防环)", loop_ok and cost < 2.0, f"{cost:.3f}s")
|
|
191
|
+
finally:
|
|
192
|
+
if old_env is None:
|
|
193
|
+
os.environ.pop(TOKEN_FILE_ENV, None)
|
|
194
|
+
else:
|
|
195
|
+
os.environ[TOKEN_FILE_ENV] = old_env
|
|
196
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
197
|
+
|
|
198
|
+
print(f"\n通过 {PASS} / 失败 {FAIL}")
|
|
199
|
+
if FAILS:
|
|
200
|
+
print("失败项:" + ",".join(FAILS))
|
|
201
|
+
return 0 if FAIL == 0 else 1
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
sys.exit(main())
|