@furongjun1999/dsh-memory 0.5.0 → 0.5.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 +552 -465
- package/docs/README.md +1 -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//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_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/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/index.js +7 -1
- 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 +582 -580
- 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 +280 -29
- package/md_cg/mdcg.py +616 -125
- package/md_cg/mdcos.py +430 -66
- 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 +170 -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_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_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_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 +6 -2
- package/src/bridge.ts +434 -401
- package/src/index.ts +526 -518
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
package/md_cg/security.py
CHANGED
|
@@ -1,275 +1,385 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""md_cg · 进程/权限模型(记忆 OS #2)
|
|
3
|
-
|
|
4
|
-
关键动机:**灵枢是开源仓库,而记忆很大一部分是私有内容**。因此权限模型必须
|
|
5
|
-
支持「公开知识」与「私有记忆」的物理与逻辑隔离:
|
|
6
|
-
|
|
7
|
-
① 物理隔离:租户(tenant)各自一个根目录。public 租户的根可落在开源仓库内,
|
|
8
|
-
private 租户的根必须落在仓库外(如 ~/.mdcg/private/)。
|
|
9
|
-
② 逻辑隔离:每个节点带 sensitivity(public < internal < private < secret);
|
|
10
|
-
每个调用方(Principal)带 clearance,只能读写 ≤ clearance 的节点。
|
|
11
|
-
③ 硬规则:secret 节点对 clearance < secret 的调用方**写入即拒**(对齐 noema
|
|
12
|
-
「secret 候选自动拒绝」);写入高于自身 clearance 的敏感度 → 拒绝。
|
|
13
|
-
|
|
14
|
-
零第三方依赖。
|
|
15
|
-
"""
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import os
|
|
20
|
-
import time
|
|
21
|
-
import uuid
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
def
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
#
|
|
227
|
-
def
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
#
|
|
252
|
-
def
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 进程/权限模型(记忆 OS #2)
|
|
3
|
+
|
|
4
|
+
关键动机:**灵枢是开源仓库,而记忆很大一部分是私有内容**。因此权限模型必须
|
|
5
|
+
支持「公开知识」与「私有记忆」的物理与逻辑隔离:
|
|
6
|
+
|
|
7
|
+
① 物理隔离:租户(tenant)各自一个根目录。public 租户的根可落在开源仓库内,
|
|
8
|
+
private 租户的根必须落在仓库外(如 ~/.mdcg/private/)。
|
|
9
|
+
② 逻辑隔离:每个节点带 sensitivity(public < internal < private < secret);
|
|
10
|
+
每个调用方(Principal)带 clearance,只能读写 ≤ clearance 的节点。
|
|
11
|
+
③ 硬规则:secret 节点对 clearance < secret 的调用方**写入即拒**(对齐 noema
|
|
12
|
+
「secret 候选自动拒绝」);写入高于自身 clearance 的敏感度 → 拒绝。
|
|
13
|
+
|
|
14
|
+
零第三方依赖。
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
from .datapath import aux_root
|
|
24
|
+
from .fsutil import publish
|
|
25
|
+
|
|
26
|
+
# 密级阶梯的体系语义(批次 27 表述正名,使用者 2026-09-24 澄清):
|
|
27
|
+
# private ≠ 个人隐私保密,而是**错误处置标记**——工作区内容因错误相关、
|
|
28
|
+
# 验证未过、待排查而被标记私有,目的是**限制向平级/下级扩散**(防止未
|
|
29
|
+
# 证实或已知有误的内容被下游消费),不是对错误处置链路保密。
|
|
30
|
+
# 可见性是**故意的冲突设计**:平级/下级限读,但错误处置链路必须可读——
|
|
31
|
+
# 设计者(designer,secret 上限)、上级节点、验证单元(verify)三者
|
|
32
|
+
# 必读。当前令牌矩阵中 record/reflect/verify/sustain/orchestr 的
|
|
33
|
+
# clearance_cap 均为 internal(forbidden 含 private)——验证单元读错误
|
|
34
|
+
# 标记节点是本职却受限于 cap,属**已登记的分级缺口**(见安全审计实锚
|
|
35
|
+
# 文档「读权限分级」节),未来专项:verify 面提 cap 或走设计者代查通道。
|
|
36
|
+
# 密级阶梯的体系语义(批次 27 正名 / 批次 28 分型落地,使用者 2026-09-24):
|
|
37
|
+
# **restricted = 错误处置标记(新增档)**——内容因错误相关、验证未过、
|
|
38
|
+
# 待排查被标记,目的是限制向平级/下级扩散;可见性按**错误处置链路角色**
|
|
39
|
+
# 判定(designer/orchestr/verify 必读,record/worker/output/guest 限读),
|
|
40
|
+
# 不走纯密级 rank(链路角色与平级同为 internal,rank 无法区分);
|
|
41
|
+
# **不加密**(不在 crypto.ENCRYPTED_LEVELS——链路要明文读)。
|
|
42
|
+
# private = 隐私/会话绑定档(语义不变:仅归属会话 + 设计者豁免可见,
|
|
43
|
+
# 落盘加密);secret = 最高密级。
|
|
44
|
+
# 可见性的故意冲突设计就此消解:错误标记限扩散但处置链路必读,
|
|
45
|
+
# 隐私内容写者私有——两种语义不再共用一个密级档。
|
|
46
|
+
SENSITIVITY_ORDER = ("public", "internal", "restricted", "private", "secret")
|
|
47
|
+
DEFAULT_SENSITIVITY = "internal"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AccessDenied(Exception):
|
|
51
|
+
"""权限拒绝(读/写/管理)。
|
|
52
|
+
|
|
53
|
+
hint(可选):与本仓 70 处失败路径 hint 同口径的「为什么 + 怎么办」可操作
|
|
54
|
+
指引(issue #34:新用户首个错误无可操作指引——guest 只读写入被拒恰是
|
|
55
|
+
三步接入后必然撞上的第一条失败,不得只说「不行」不说「怎么办」)。
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, msg: str, hint: str = None):
|
|
59
|
+
super().__init__(msg)
|
|
60
|
+
self.hint = hint
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# 生效条件:形参 level 为模块级常量 SENSITIVITY_ORDER 中的元素时返回其下标,否则抛 AccessDenied。
|
|
64
|
+
def _rank(level: str) -> int:
|
|
65
|
+
try:
|
|
66
|
+
return SENSITIVITY_ORDER.index(level)
|
|
67
|
+
except ValueError:
|
|
68
|
+
raise AccessDenied(f"未知敏感度/密级:{level}") from None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# 生效条件:全部形参均可省略、clearance 默认取模块常量 DEFAULT_SENSITIVITY,构造时先经 _rank(clearance) 校验,随后 session 为假值(含空串)回落为 sess_+uuid 十二位、role 假值回落 "system"、expires_at 为假值(含 0)置 None、layers_allow/ops_allow 为 None 时保持 None 否则转 tuple、theory_ok 经 bool() 转换。
|
|
72
|
+
class Principal:
|
|
73
|
+
"""调用方身份(一次会话一个)。
|
|
74
|
+
|
|
75
|
+
tenant —— 租户(决定根目录)
|
|
76
|
+
actor —— 调用者标识(写入审计)
|
|
77
|
+
clearance —— 最高可读/可写敏感度
|
|
78
|
+
can_write —— 是否允许写
|
|
79
|
+
can_admin —— 是否允许管理操作(forget/restore/review_decide)
|
|
80
|
+
session —— 会话 id(进程/会话模型:每次连接一个)
|
|
81
|
+
harness —— 承载端标识(dsh/zcode/codebuddy…;仅归因,不参与授权)
|
|
82
|
+
unit —— 单元分工(record/reflect/verify/output/sustain;仅归因)
|
|
83
|
+
注:MCP 请求级参数 `as_unit` 会经 `tokens.narrowed_principal`
|
|
84
|
+
产出一次性 Principal——其 role/layers/ops 取自该单元 spec、unit
|
|
85
|
+
记为执行单元。那是**单次调用的临时身份**,不改变本字段的归因
|
|
86
|
+
定位(见 `mcp_server.call_tool`)。
|
|
87
|
+
|
|
88
|
+
令牌扩展(由 `tokens.verify_token` 填充;直接构造时为 None = 不限制):
|
|
89
|
+
role —— 角色(designer/reflection/verifier/recorder/output/sustain/guest)
|
|
90
|
+
token_id —— 令牌 id(可追溯到签发记录)
|
|
91
|
+
parent —— 派生来源令牌 id(权职分离的委派链)
|
|
92
|
+
expires_at —— 过期时间戳(None = 不过期)
|
|
93
|
+
layers_allow —— 可写层白名单(None = 不限制;"*" = 全部)
|
|
94
|
+
ops_allow —— 可执行 op 白名单(同上)
|
|
95
|
+
auth_mode —— direct(直接构造)/ token / legacy_env / anonymous
|
|
96
|
+
|
|
97
|
+
版本层扩展(蜂群互联层0,由 `theory.check` 填充):
|
|
98
|
+
theory_ok —— 版本声明是否合法;False 时**全部写/管理操作被拒**(只读降级)
|
|
99
|
+
theory_version —— 当前声明的协议版本(审计与 whoami 用)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# 生效条件:clearance 须为模块级常量 SENSITIVITY_ORDER 成员(否则 _rank 抛 AccessDenied),session 为假值(含 None/空串)时生成 sess_ 随机串,expires_at 为假值(含 None/0)时存 None 否则 float(expires_at),layers_allow/ops_allow 为 None 时存 None 否则 tuple 化。
|
|
103
|
+
def __init__(self, tenant: str = "default", actor: str = "system",
|
|
104
|
+
clearance: str = DEFAULT_SENSITIVITY, can_write: bool = True,
|
|
105
|
+
can_admin: bool = False, session: str = None,
|
|
106
|
+
harness: str = None, unit: str = None,
|
|
107
|
+
role: str = None, token_id: str = None, parent: str = None,
|
|
108
|
+
expires_at: float = None, layers_allow=None, ops_allow=None,
|
|
109
|
+
auth_mode: str = "direct", theory_ok: bool = True,
|
|
110
|
+
theory_version: str = None):
|
|
111
|
+
_rank(clearance) # 校验
|
|
112
|
+
self.tenant = tenant
|
|
113
|
+
self.actor = actor
|
|
114
|
+
self.clearance = clearance
|
|
115
|
+
self.can_write = can_write
|
|
116
|
+
self.can_admin = can_admin
|
|
117
|
+
self.session = session or ("sess_" + uuid.uuid4().hex[:12])
|
|
118
|
+
# 归因维度(嵌套身份):只入审计(_audit/_recent),不参与权限判定。
|
|
119
|
+
# 权限域仍由令牌记录决定(tokens.ROLE_SPECS),与 harness/unit 无关。
|
|
120
|
+
# 受控例外:MCP 请求级 `as_unit` 收窄(tokens.narrowed_principal)产出的是
|
|
121
|
+
# 一次性 Principal,其 role/allow 取自单元 spec——**只做减法**(与 owner
|
|
122
|
+
# 求交 + 管理权恒 False),故不构成本字段「参与授权」的先例。
|
|
123
|
+
self.harness = harness
|
|
124
|
+
self.unit = unit
|
|
125
|
+
self.role = (role or "system")
|
|
126
|
+
self.token_id = token_id
|
|
127
|
+
self.parent = parent
|
|
128
|
+
self.expires_at = float(expires_at) if expires_at else None
|
|
129
|
+
self.layers_allow = None if layers_allow is None else tuple(layers_allow)
|
|
130
|
+
self.ops_allow = None if ops_allow is None else tuple(ops_allow)
|
|
131
|
+
self.auth_mode = auth_mode
|
|
132
|
+
self.theory_ok = bool(theory_ok)
|
|
133
|
+
self.theory_version = theory_version
|
|
134
|
+
|
|
135
|
+
# ---------- 基础判定 ----------
|
|
136
|
+
|
|
137
|
+
# 生效条件:形参 sensitivity 与 self.clearance 均可被 _rank 映射到模块级常量 SENSITIVITY_ORDER 中时,返回前者排名是否不高于后者;任一不在其中则 _rank 抛 AccessDenied。
|
|
138
|
+
def allows(self, sensitivity: str) -> bool:
|
|
139
|
+
"""clearance 是否覆盖该敏感度(可读/可写)。"""
|
|
140
|
+
return _rank(sensitivity) <= _rank(self.clearance)
|
|
141
|
+
|
|
142
|
+
# 生效条件:无 required 形参或模块级常量前置,仅当 self.expires_at(来自 __init__ 的 expires_at)不为 None 且 time.time() 大于它时返回 True,否则返回 False。
|
|
143
|
+
def expired(self) -> bool:
|
|
144
|
+
return self.expires_at is not None and time.time() > self.expires_at
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
# 生效条件:allow 为 None 时返回 True(未声明=不限制),allow 非 None 时返回 "'*' in allow 或 name in allow" 的布尔结果。
|
|
148
|
+
def _in_scope(allow, name: str) -> bool:
|
|
149
|
+
if allow is None: # 未声明 = 不限制(兼容直接构造)
|
|
150
|
+
return True
|
|
151
|
+
return "*" in allow or name in allow
|
|
152
|
+
|
|
153
|
+
# 生效条件:layer 为假值(None/空串)时以 "knowledge" 参与判定,返回 self._in_scope(self.layers_allow, layer or "knowledge") 的结果。
|
|
154
|
+
def allows_layer(self, layer: str) -> bool:
|
|
155
|
+
return self._in_scope(self.layers_allow, layer or "knowledge")
|
|
156
|
+
|
|
157
|
+
# 生效条件:op 为假值(None/空串)时以 "" 参与,先经 strip().lower() 归一化,返回 self._in_scope(self.ops_allow, (op or "").strip().lower()) 的结果。
|
|
158
|
+
def allows_op(self, op: str) -> bool:
|
|
159
|
+
return self._in_scope(self.ops_allow, (op or "").strip().lower())
|
|
160
|
+
|
|
161
|
+
# ---------- 强制校验(越权即 AccessDenied) ----------
|
|
162
|
+
|
|
163
|
+
# 生效条件:无 required 形参或模块级常量前置,当 self.expired()(来自 __init__ 的 expires_at 与当前时间比较)为 True 时抛 AccessDenied(带重新签发 hint),否则无操作返回 None。
|
|
164
|
+
def _require_live(self):
|
|
165
|
+
if self.expired():
|
|
166
|
+
raise AccessDenied(
|
|
167
|
+
f"actor={self.actor} 令牌已过期",
|
|
168
|
+
hint="令牌过期不是故障:重新签发并更新 MDCG_TOKEN 后重启宿主"
|
|
169
|
+
"(python -m md_cg.tokens issue …,README「写入凭据」一节)。")
|
|
170
|
+
|
|
171
|
+
# issue #34:op 拒绝按成因分指引——guest(无令牌只读访客)引导配凭据;
|
|
172
|
+
# 有令牌但白名单不含该 op 引导换角色/补授权。「不是故障」安抚语气与
|
|
173
|
+
# 本仓其它 70 处 hint 同口径。
|
|
174
|
+
_GUEST_OP_HINT = (
|
|
175
|
+
"当前为只读访客模式(未检测到写入凭据 MDCG_TOKEN):读/召回/时间线可用,"
|
|
176
|
+
"写入与管理操作不落盘。这不是故障,配置凭据后即恢复。"
|
|
177
|
+
"要真正落盘请签发并配置写入凭据(README「写入凭据」一节):\n"
|
|
178
|
+
" python -m md_cg.tokens issue --role recorder --actor <名> "
|
|
179
|
+
"--clearance internal\n"
|
|
180
|
+
" setx MDCG_TOKEN \"<token>\" # 然后重启宿主(DSH/IDE)\n"
|
|
181
|
+
"最小权限可用 --role recorder;op 用法可查 cg(op=\"help\", query=\"write\")。")
|
|
182
|
+
|
|
183
|
+
# 生效条件:先调用 self._require_live()(过期则抛 AccessDenied);再要求 self.allows_op(op) 为 True,否则抛 AccessDenied(role=guest 带配凭据 hint,其它角色带补授权 hint)。
|
|
184
|
+
def require_op(self, op: str):
|
|
185
|
+
self._require_live()
|
|
186
|
+
if not self.allows_op(op):
|
|
187
|
+
msg = (f"角色 {self.role} 无权执行 op={op}(作用域 "
|
|
188
|
+
f"{list(self.ops_allow) if self.ops_allow is not None else '不限'})")
|
|
189
|
+
if self.role == "guest":
|
|
190
|
+
raise AccessDenied(msg, hint=self._GUEST_OP_HINT)
|
|
191
|
+
raise AccessDenied(
|
|
192
|
+
msg,
|
|
193
|
+
hint=f"当前令牌(actor={self.actor},角色 {self.role})的 ops "
|
|
194
|
+
f"白名单不含 {op},属正常闸门不是故障。可:①换用含该 op "
|
|
195
|
+
f"的角色令牌;②由签发者以 --ops-allow 补授权后重新签发"
|
|
196
|
+
f"(python -m md_cg.tokens issue …,README「写入凭据」一节)。")
|
|
197
|
+
|
|
198
|
+
# 生效条件:依次要求 self._require_live() 未抛异常、self.theory_ok 为 True、self.can_write 为 True、self.allows(sensitivity) 为 True;任一不满足则抛 AccessDenied。
|
|
199
|
+
def require_write(self, sensitivity: str):
|
|
200
|
+
self._require_live()
|
|
201
|
+
if not self.theory_ok:
|
|
202
|
+
raise AccessDenied(
|
|
203
|
+
"版本层校验未通过(theory_ok=False):全部写操作被拒,"
|
|
204
|
+
"仅保留 theory 修复入口")
|
|
205
|
+
if not self.can_write:
|
|
206
|
+
raise AccessDenied(
|
|
207
|
+
f"actor={self.actor} 无写权限",
|
|
208
|
+
hint=(self._GUEST_OP_HINT if self.role == "guest" else
|
|
209
|
+
f"当前令牌(角色 {self.role})未授予写权限(can_write="
|
|
210
|
+
f"False),属正常闸门不是故障。请换用可写角色令牌或由"
|
|
211
|
+
f"签发者重新签发(README「写入凭据」一节)。"))
|
|
212
|
+
if not self.allows(sensitivity):
|
|
213
|
+
raise AccessDenied(
|
|
214
|
+
f"写入敏感度 {sensitivity} 超出 clearance {self.clearance}")
|
|
215
|
+
|
|
216
|
+
# 生效条件:先要求 self.require_write(sensitivity) 未抛异常;随后将形参 layer 为假值(None/空串)时按 "knowledge" 处理,并要求 self.allows_layer(layer) 为 True,否则抛 AccessDenied。
|
|
217
|
+
def require_layer_write(self, layer: str, sensitivity: str):
|
|
218
|
+
"""写层校验:密级 + 层白名单双闸门(核心私有内容不可越权修改)。"""
|
|
219
|
+
self.require_write(sensitivity)
|
|
220
|
+
layer = layer or "knowledge"
|
|
221
|
+
if not self.allows_layer(layer):
|
|
222
|
+
raise AccessDenied(
|
|
223
|
+
f"角色 {self.role} 无权写入 {layer} 层"
|
|
224
|
+
f"(可写层 {list(self.layers_allow) if self.layers_allow is not None else '不限'})")
|
|
225
|
+
|
|
226
|
+
# 生效条件:要求 self._require_live() 未抛异常、self.theory_ok 为 True、self.can_admin 为 True 时通过;否则抛 AccessDenied(消息用形参 op 标记操作)。
|
|
227
|
+
def require_admin(self, op: str):
|
|
228
|
+
self._require_live()
|
|
229
|
+
if not self.theory_ok:
|
|
230
|
+
raise AccessDenied(
|
|
231
|
+
f"版本层校验未通过(theory_ok=False):管理操作 {op} 被拒")
|
|
232
|
+
if not self.can_admin:
|
|
233
|
+
raise AccessDenied(f"actor={self.actor} 无管理权限({op})")
|
|
234
|
+
|
|
235
|
+
# 生效条件:无 required 形参或模块级常量前置,返回包含 self 各属性(tenant/actor/clearance/can_write/can_admin/session/harness/unit/role/token_id/parent/auth_mode/expires_at/theory_ok/theory_version 及 layers_allow/ops_allow)的 dict;其中 layers_allow/ops_allow 为 None 时值为 None,否则 list 化。
|
|
236
|
+
def as_dict(self):
|
|
237
|
+
return {"tenant": self.tenant, "actor": self.actor,
|
|
238
|
+
"clearance": self.clearance, "can_write": self.can_write,
|
|
239
|
+
"can_admin": self.can_admin, "session": self.session,
|
|
240
|
+
"harness": self.harness, "unit": self.unit,
|
|
241
|
+
"role": self.role, "token_id": self.token_id,
|
|
242
|
+
"parent": self.parent, "auth_mode": self.auth_mode,
|
|
243
|
+
"expires_at": self.expires_at,
|
|
244
|
+
"theory_ok": self.theory_ok,
|
|
245
|
+
"theory_version": self.theory_version,
|
|
246
|
+
"layers_allow": (None if self.layers_allow is None
|
|
247
|
+
else list(self.layers_allow)),
|
|
248
|
+
"ops_allow": (None if self.ops_allow is None
|
|
249
|
+
else list(self.ops_allow))}
|
|
250
|
+
|
|
251
|
+
# 生效条件:无前置;仅返回 tenant/actor/role/clearance/write/admin 的短摘要用于日志与排障,不含 token、密钥材料与能力白名单明细;
|
|
252
|
+
def __repr__(self):
|
|
253
|
+
return (f"Principal(tenant={self.tenant!r}, actor={self.actor!r}, "
|
|
254
|
+
f"role={self.role!r}, clearance={self.clearance!r}, "
|
|
255
|
+
f"write={self.can_write}, admin={self.can_admin})")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# 生效条件:path 为 None 时落至 datapath.aux_root()(默认 ~/.mdcg,可经 MDCG_AUX_ROOT 改)下的 _tenants.json,path 非 None(含空串)时按传入值使用,并在构造内以 self._load() 的返回填充 self.data、以 self.load_error 留存加载期损坏留痕(N124,2026-09-25:登记表损坏/不可读必须可被调用方感知,缺文件不算损坏)。
|
|
259
|
+
class TenantRegistry:
|
|
260
|
+
"""租户注册表:tenant → {root, clearance_cap, description}。
|
|
261
|
+
|
|
262
|
+
默认位置:<registry_dir>/_tenants.json(默认 ~/.mdcg/)。
|
|
263
|
+
设计意图:私有租户的 root 指向仓库外目录,开源仓库里只放 public 租户的根。
|
|
264
|
+
|
|
265
|
+
load_error(N124,2026-09-25):_load 遇「文件在但损坏/不可读」(JSON
|
|
266
|
+
解析失败、OSError、顶层非对象)时留存原因字符串,完好/缺文件为 None——
|
|
267
|
+
供 _resolve_root 等调用方把「静默回落空表」开口成 stderr 告警。
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
# 生效条件:形参 path 为 None 时取 ~/.mdcg/_tenants.json,否则取 path;self.load_error 置 None 后 self.data 初始化为 _load() 结果(self.path 经 os.path.exists 为真且 JSON 解析为 dict 时取该 dict,否则回落 {"schema":1,"tenants":{}};文件在但解析失败/OSError/顶层非对象时先置 self.load_error 为原因串,缺文件不置)。
|
|
271
|
+
def __init__(self, path: str = None):
|
|
272
|
+
if path is None:
|
|
273
|
+
path = os.path.join(aux_root(), "_tenants.json")
|
|
274
|
+
self.path = path
|
|
275
|
+
self.load_error = None
|
|
276
|
+
self.data = self._load()
|
|
277
|
+
|
|
278
|
+
# 生效条件:先置 self.load_error=None;当 self.path 经 os.path.exists 为真且内容可解析为 dict 时返回该 dict;解析失败或 OSError 时置 self.load_error=f"{类型名}: {e}"、顶层 JSON 非对象时置 self.load_error="登记表 JSON 顶层不是对象";否则(os.path.exists 为假)不留痕;各回落分支均返回 {"schema":1,"tenants":{}}。
|
|
279
|
+
def _load(self):
|
|
280
|
+
self.load_error = None
|
|
281
|
+
if os.path.exists(self.path):
|
|
282
|
+
try:
|
|
283
|
+
with open(self.path, encoding="utf-8") as f:
|
|
284
|
+
d = json.load(f)
|
|
285
|
+
if isinstance(d, dict):
|
|
286
|
+
return d
|
|
287
|
+
self.load_error = "登记表 JSON 顶层不是对象"
|
|
288
|
+
except (ValueError, OSError) as e:
|
|
289
|
+
self.load_error = f"{type(e).__name__}: {e}"
|
|
290
|
+
return {"schema": 1, "tenants": {}}
|
|
291
|
+
|
|
292
|
+
# 生效条件:无 required 形参或模块级常量前置,将 self.data 以 JSON 写入 self.path + ".tmp",随后 publish(带 Windows 短重试的 os.replace)到 self.path;目录名称为空时用 "." 创建。
|
|
293
|
+
def _save(self):
|
|
294
|
+
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
|
|
295
|
+
tmp = self.path + ".tmp"
|
|
296
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
297
|
+
json.dump(self.data, f, ensure_ascii=False, indent=1)
|
|
298
|
+
publish(tmp, self.path)
|
|
299
|
+
|
|
300
|
+
# 生效条件:形参 clearance_cap 须为模块级常量 SENSITIVITY_ORDER 成员(否则 _rank 抛 AccessDenied);形参 tenant/root 提供后写入 self.data["tenants"](要求 self.data 含 "tenants" 键,否则 KeyError),_save 成功则返回新登记项。
|
|
301
|
+
def register(self, tenant: str, root: str, clearance_cap: str = DEFAULT_SENSITIVITY,
|
|
302
|
+
description: str = ""):
|
|
303
|
+
_rank(clearance_cap)
|
|
304
|
+
self.data["tenants"][tenant] = {
|
|
305
|
+
"root": os.path.abspath(root),
|
|
306
|
+
"clearance_cap": clearance_cap,
|
|
307
|
+
"description": description,
|
|
308
|
+
"registered_at": time.time(),
|
|
309
|
+
}
|
|
310
|
+
self._save()
|
|
311
|
+
return self.data["tenants"][tenant]
|
|
312
|
+
|
|
313
|
+
# 生效条件:self.data 含 "tenants" 键(否则 KeyError),且该映射中存在形参 tenant 时返回其值,否则返回 None(.get 缺键回落 None,键存在值为 None 也返回 None)。
|
|
314
|
+
def get(self, tenant: str):
|
|
315
|
+
return self.data["tenants"].get(tenant)
|
|
316
|
+
|
|
317
|
+
# 生效条件:self.get(tenant) 返回真值(非 None/空 dict 等)时返回 t["root"](若 t 无 "root" 键则 KeyError);返回假值时返回 None。
|
|
318
|
+
def root_of(self, tenant: str):
|
|
319
|
+
t = self.get(tenant)
|
|
320
|
+
return t["root"] if t else None
|
|
321
|
+
|
|
322
|
+
# 生效条件:self.get(tenant) 返回真值时返回 t["clearance_cap"](缺键则 KeyError);返回假值时返回模块级常量 DEFAULT_SENSITIVITY。
|
|
323
|
+
def cap_of(self, tenant: str):
|
|
324
|
+
t = self.get(tenant)
|
|
325
|
+
return t["clearance_cap"] if t else DEFAULT_SENSITIVITY
|
|
326
|
+
|
|
327
|
+
# 生效条件:self.data 含 "tenants" 键时返回其浅拷贝 dict(self.data["tenants"]);该键缺失时按 self.data["tenants"] 取值会 KeyError,无默认回落。
|
|
328
|
+
def all(self):
|
|
329
|
+
return dict(self.data["tenants"])
|
|
330
|
+
|
|
331
|
+
# 生效条件:cap 由 cap_of(tenant) 决定(形参 tenant 未注册时取模块级常量 DEFAULT_SENSITIVITY);clearance 为假值(含 None/空串)时 want 取 cap,否则先取 clearance,再在 _rank(want) > _rank(cap) 时夹紧为 cap;actor 为假值时取 tenant;返回 Principal(...)。
|
|
332
|
+
def principal_for(self, tenant: str, actor: str = None, clearance: str = None,
|
|
333
|
+
can_write: bool = True, can_admin: bool = False,
|
|
334
|
+
session: str = None) -> Principal:
|
|
335
|
+
"""按租户上限夹紧 clearance(调用方不能超过租户上限)。"""
|
|
336
|
+
cap = self.cap_of(tenant)
|
|
337
|
+
want = clearance or cap
|
|
338
|
+
if _rank(want) > _rank(cap):
|
|
339
|
+
want = cap
|
|
340
|
+
return Principal(tenant=tenant, actor=actor or tenant, clearance=want,
|
|
341
|
+
can_write=can_write, can_admin=can_admin, session=session)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# P1-4(批次 26,外部审查报告):工具路径参数的根白名单——ingest/export/link
|
|
345
|
+
# 的 path/out 是模型可控输入,原样透传 = 任意文件读(/etc/passwd 进图)与
|
|
346
|
+
# 任意文件写(evidence export 的 out)。校验语义与 HIVE_READ_ROOTS 同构:
|
|
347
|
+
# **env 未设置 = 放开**(默认部署零行为变更,与 2026-09-19 的读放开裁定
|
|
348
|
+
# 精神一致);**设置了 = realpath 落根内否则拒**(fail-closed),`..` 与
|
|
349
|
+
# 绝对路径穿越在 realpath 归一后自然被涵盖。支持 os.pathsep 分隔多根。
|
|
350
|
+
# 生效条件:env_var 环境变量去空白后为空、或 path 为 None/空串时直接返回(不约束);否则取 realpath(expanduser(path)),与 env 值按 os.pathsep 切分的每个根做「相等或以根+os.sep 为前缀」判定,任一命中即返回;全部未命中抛 PermissionError(含 what/原 path/realpath/roots 的拒绝说明)。
|
|
351
|
+
def check_path_root(path, env_var: str, what: str) -> None:
|
|
352
|
+
raw = (os.environ.get(env_var) or "").strip()
|
|
353
|
+
if not raw:
|
|
354
|
+
return # 未配置 = 放开(部署开关)
|
|
355
|
+
if path is None or str(path).strip() == "":
|
|
356
|
+
return # 无路径参数(如 stat/action)
|
|
357
|
+
roots = []
|
|
358
|
+
for p in raw.split(os.pathsep):
|
|
359
|
+
p = p.strip()
|
|
360
|
+
if p:
|
|
361
|
+
roots.append(os.path.realpath(os.path.expanduser(p)))
|
|
362
|
+
real = os.path.realpath(os.path.expanduser(str(path)))
|
|
363
|
+
for r in roots:
|
|
364
|
+
if real == r or real.startswith(r.rstrip(os.sep) + os.sep):
|
|
365
|
+
return
|
|
366
|
+
raise PermissionError(
|
|
367
|
+
f"{what} 路径超出 {env_var} 白名单,拒绝访问:{path} "
|
|
368
|
+
f"(realpath={real}, roots={roots})——路径类工具参数的根约束"
|
|
369
|
+
"(P1-4),部署用该环境变量声明可读写的根目录")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# 错误处置链路角色集(批次 28 分型):restricted(错误处置标记)的可见性
|
|
373
|
+
# 不走密级 rank——链路角色与平级同为 internal,rank 无法区分「处置者」与
|
|
374
|
+
# 「扩散面」。designer=全局观测本职;orchestr=上级节点(拆解派发/裁决
|
|
375
|
+
# 收口);verify=验证本职(错误标记节点是验证的对象)。
|
|
376
|
+
_RESTRICTED_READER_ROLES = ("designer", "orchestr", "verify")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# 生效条件:p 为 Principal 或 None;p 为 None 返回 False;p.can_admin 为真(设计者)或 p.role 属 _RESTRICTED_READER_ROLES 时返回 True,否则返回 False。
|
|
380
|
+
def can_read_restricted(p) -> bool:
|
|
381
|
+
if p is None:
|
|
382
|
+
return False
|
|
383
|
+
if getattr(p, "can_admin", False):
|
|
384
|
+
return True
|
|
385
|
+
return getattr(p, "role", "") in _RESTRICTED_READER_ROLES
|