@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
package/md_cg/mcp_server.py
CHANGED
|
@@ -59,10 +59,11 @@ def _s(_desc, **props):
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
# 生效条件:始终返回 {"type":t,"description":desc},仅当 req 为真值时该字典额外含 "_req": True。
|
|
62
|
-
def _p(t, desc, req=False):
|
|
62
|
+
def _p(t, desc, req=False, **extra):
|
|
63
63
|
d = {"type": t, "description": desc}
|
|
64
64
|
if req:
|
|
65
65
|
d["_req"] = True
|
|
66
|
+
d.update(extra) # P2-22 前置(批次 24):node_id 等可传 pattern/maxLength
|
|
66
67
|
return d
|
|
67
68
|
|
|
68
69
|
|
|
@@ -106,7 +107,9 @@ TOOLS = [
|
|
|
106
107
|
"适用于写入情景层记忆时的筛选(未指定 layer 时默认 contextual)。"
|
|
107
108
|
"写入恒带会话身份(session)以区分不同会话的记忆;省略则归因到"
|
|
108
109
|
"进程/环境身份。",
|
|
109
|
-
"inputSchema": _s("", node_id=_p("string", "节点 id(省略则自动生成)"
|
|
110
|
+
"inputSchema": _s("", node_id=_p("string", "节点 id(省略则自动生成)",
|
|
111
|
+
pattern="^[A-Za-z0-9_.@\u4e00-\u9fff,-]{1,128}$",
|
|
112
|
+
maxLength=128),
|
|
110
113
|
content=_p("string", "节点内容", True),
|
|
111
114
|
layer=_p("string", "层:anchor|structural|knowledge|contextual|self"),
|
|
112
115
|
role=_p("string", "角色:knowledge|user|assistant|tool-output|command|edit"
|
|
@@ -179,7 +182,10 @@ TOOLS = [
|
|
|
179
182
|
},
|
|
180
183
|
{
|
|
181
184
|
"name": "mdcg_get",
|
|
182
|
-
"description": "按 id 读取一个记忆节点(frontmatter + content)。"
|
|
185
|
+
"description": "按 id 读取一个记忆节点(frontmatter + content)。"
|
|
186
|
+
"id 不存在或密级不可读时返回 {ok:false, error:'not_found', "
|
|
187
|
+
"readable_sensitivities, hint}(不再返回裸 null——"
|
|
188
|
+
"调用方须能区分「没有」与「读不到」)。",
|
|
183
189
|
"inputSchema": _s("", node_id=_p("string", "节点 id", True)),
|
|
184
190
|
},
|
|
185
191
|
{
|
|
@@ -191,10 +197,13 @@ TOOLS = [
|
|
|
191
197
|
{
|
|
192
198
|
"name": "mdcg_verify",
|
|
193
199
|
"description": "验证单元:对节点做外部裁决 confirmed/weakened/falsified"
|
|
194
|
-
"(falsified 会移入 rejected/
|
|
200
|
+
"(falsified 会移入 rejected/ 负记忆;受保护节点"
|
|
201
|
+
" self/anchor 层、protected 标记、importance≥0.7 不可"
|
|
202
|
+
"一键证伪删除,需显式 override=true,旧版本自动快照+留痕)。",
|
|
195
203
|
"inputSchema": _s("", node_id=_p("string", "节点 id", True),
|
|
196
204
|
evidence=_p("string", "证据", True),
|
|
197
|
-
verdict=_p("string", "confirmed|weakened|falsified", True)
|
|
205
|
+
verdict=_p("string", "confirmed|weakened|falsified", True),
|
|
206
|
+
override=_p("boolean", "受保护节点需显式 true 才可证伪删除")),
|
|
198
207
|
},
|
|
199
208
|
{
|
|
200
209
|
"name": "mdcg_flywheel",
|
|
@@ -233,7 +242,9 @@ TOOLS = [
|
|
|
233
242
|
},
|
|
234
243
|
{
|
|
235
244
|
"name": "mdcg_review_decide",
|
|
236
|
-
"description": "审核裁决:accept / reject / edit / merge / noop(merge 需 merge_into
|
|
245
|
+
"description": "审核裁决:accept / reject / edit / merge / noop(merge 需 merge_into;"
|
|
246
|
+
"merge 与 add 同受写保护——目标层须可写,self/anchor/immutable "
|
|
247
|
+
"目标需显式 override=true)。"
|
|
237
248
|
"noop=已评估、判定不改变任何现有记忆:只留痕并关闭提案,"
|
|
238
249
|
"不落业务节点、不进负记忆(与 reject 的区别是语义而非路径)。"
|
|
239
250
|
"redteam.verdict=reject 不落库,转 needs_reapproval,"
|
|
@@ -243,7 +254,8 @@ TOOLS = [
|
|
|
243
254
|
edits=_p("object", "edit 时的覆盖字段"), merge_into=_p("string", "merge 目标节点 id"),
|
|
244
255
|
reason=_p("string", "裁决理由"),
|
|
245
256
|
redteam=_p("object", "红队裁决 {verdict:pass|reject, issues:[], round:n}"),
|
|
246
|
-
issues=_p("array", "问题清单(打回理由)")
|
|
257
|
+
issues=_p("array", "问题清单(打回理由)"),
|
|
258
|
+
override=_p("boolean", "merge 进受保护目标需显式 true")),
|
|
247
259
|
},
|
|
248
260
|
{
|
|
249
261
|
"name": "mdcg_review_records",
|
|
@@ -1084,6 +1096,15 @@ def _trust_state(fm):
|
|
|
1084
1096
|
return None
|
|
1085
1097
|
|
|
1086
1098
|
|
|
1099
|
+
# 生效条件:cg 具有 whoami() 且其返回 dict 含 readable_sensitivities 时返回该值;cg 无该方法或抛异常时返回 None(不编造、不抛)。
|
|
1100
|
+
def _readable_sensitivities(cg):
|
|
1101
|
+
"""当前身份可读密级(whoami 同源;取不到返回 None,不编造)。"""
|
|
1102
|
+
try:
|
|
1103
|
+
return (cg.whoami() or {}).get("readable_sensitivities")
|
|
1104
|
+
except Exception:
|
|
1105
|
+
return None
|
|
1106
|
+
|
|
1107
|
+
|
|
1087
1108
|
def _node_view(node, offset: int = 0):
|
|
1088
1109
|
if not node:
|
|
1089
1110
|
return None
|
|
@@ -2021,6 +2042,10 @@ def _cg_dispatch(cg, a):
|
|
|
2021
2042
|
limit=int(a.get("limit") or a.get("k") or 100))
|
|
2022
2043
|
if act in ("export", "evidence_export"):
|
|
2023
2044
|
from . import evidence as _ev
|
|
2045
|
+
from .security import check_path_root
|
|
2046
|
+
# P1-4(批次 26):证据包写/读路径根约束(报告点名的
|
|
2047
|
+
# 「link export 不经 require_admin → 任意文件写」缺口)
|
|
2048
|
+
check_path_root(a.get("out"), "MDCG_EXPORT_ROOT", "link/export")
|
|
2024
2049
|
subs = a.get("subjects") or a.get("subject")
|
|
2025
2050
|
if isinstance(subs, str):
|
|
2026
2051
|
subs = [subs]
|
|
@@ -2034,6 +2059,9 @@ def _cg_dispatch(cg, a):
|
|
|
2034
2059
|
return res
|
|
2035
2060
|
if act in ("import", "evidence_import"):
|
|
2036
2061
|
from . import evidence as _ev
|
|
2062
|
+
from .security import check_path_root
|
|
2063
|
+
check_path_root(a.get("path") or a.get("pack"),
|
|
2064
|
+
"MDCG_EXPORT_ROOT", "link/import")
|
|
2037
2065
|
src = a.get("path") or a.get("pack")
|
|
2038
2066
|
if isinstance(src, str) and src.lstrip().startswith("{"):
|
|
2039
2067
|
src = json.loads(src)
|
|
@@ -2123,7 +2151,9 @@ def _cg_dispatch(cg, a):
|
|
|
2123
2151
|
return _node_view(cg.get(a["node_id"]),
|
|
2124
2152
|
offset=int(a.get("offset") or 0))
|
|
2125
2153
|
q = a.get("query") or a.get("intent") or ""
|
|
2126
|
-
if a.get("budget_tokens")
|
|
2154
|
+
# 显式 0 也是「显式」(2026-09-24 修复):旧写法 `if a.get("budget_tokens")`
|
|
2155
|
+
# 把 0 当没传,静默换成 1200 —— 调用方以为「零预算」拿到的是满预算结果。
|
|
2156
|
+
if a.get("budget_tokens") is not None:
|
|
2127
2157
|
return cg.recall(q, budget_tokens=int(a["budget_tokens"]),
|
|
2128
2158
|
k=int(a.get("k") or 20), context=a.get("context"),
|
|
2129
2159
|
goal_text=a.get("goal"),
|
|
@@ -2185,7 +2215,8 @@ def _cg_dispatch(cg, a):
|
|
|
2185
2215
|
|
|
2186
2216
|
if op == "verify":
|
|
2187
2217
|
return cg.verify(a.get("node_id", ""), a.get("evidence", ""),
|
|
2188
|
-
a.get("verdict", "")
|
|
2218
|
+
a.get("verdict", ""),
|
|
2219
|
+
override=bool(a.get("override")))
|
|
2189
2220
|
|
|
2190
2221
|
if op == "review":
|
|
2191
2222
|
act = (a.get("action") or "list").strip().lower()
|
|
@@ -2205,7 +2236,8 @@ def _cg_dispatch(cg, a):
|
|
|
2205
2236
|
return cg.review_decide(a.get("pid", ""), a.get("decision", ""),
|
|
2206
2237
|
edits=a.get("edits"), merge_into=a.get("merge_into"),
|
|
2207
2238
|
reason=a.get("reason", ""),
|
|
2208
|
-
redteam=a.get("redteam"), issues=a.get("issues")
|
|
2239
|
+
redteam=a.get("redteam"), issues=a.get("issues"),
|
|
2240
|
+
override=bool(a.get("override")))
|
|
2209
2241
|
|
|
2210
2242
|
if op == "forget":
|
|
2211
2243
|
if (a.get("action") or "forget").strip().lower() == "restore":
|
|
@@ -2248,7 +2280,12 @@ def _cg_dispatch(cg, a):
|
|
|
2248
2280
|
|
|
2249
2281
|
if op == "index_code":
|
|
2250
2282
|
from . import refindex
|
|
2283
|
+
from .security import check_path_root
|
|
2251
2284
|
root = a.get("path") or cg.root
|
|
2285
|
+
# P1-X:path 是模型可控输入——索引会读取目录下全部
|
|
2286
|
+
# 命中文件。根白名单与 ingest 的 P1-4 同语义:显式 path 越界即拒;
|
|
2287
|
+
# 未传(回落服务器侧 cg.root)不校验,默认部署行为不变。
|
|
2288
|
+
check_path_root(a.get("path"), "MDCG_INGEST_ROOT", "index_code")
|
|
2252
2289
|
if not os.path.isdir(root):
|
|
2253
2290
|
return {"ok": False, "error": f"目录不存在:{root}"}
|
|
2254
2291
|
items, errors, stats = refindex.index_dir(
|
|
@@ -2280,7 +2317,11 @@ def _cg_dispatch(cg, a):
|
|
|
2280
2317
|
|
|
2281
2318
|
if op == "index_doc":
|
|
2282
2319
|
from . import refindex
|
|
2320
|
+
from .security import check_path_root
|
|
2283
2321
|
root = a.get("path") or cg.root
|
|
2322
|
+
# P1-X:与 index_code 同族——path 模型可控,根白名单
|
|
2323
|
+
# 越界即拒;未传(回落 cg.root)不校验,默认部署行为不变。
|
|
2324
|
+
check_path_root(a.get("path"), "MDCG_INGEST_ROOT", "index_doc")
|
|
2284
2325
|
if not os.path.isdir(root):
|
|
2285
2326
|
return {"ok": False, "error": f"目录不存在:{root}"}
|
|
2286
2327
|
layer = a.get("layer") or "knowledge"
|
|
@@ -2542,7 +2583,9 @@ def _session_call(cg, a):
|
|
|
2542
2583
|
return cg.session_recall(
|
|
2543
2584
|
session=a.get("session"), limit=int(a.get("limit") or 5),
|
|
2544
2585
|
recent_limit=int(a.get("recent_limit") or 10),
|
|
2545
|
-
|
|
2586
|
+
# `is not None`:显式 0 不再被吞成默认 1200(2026-09-24 修复)
|
|
2587
|
+
budget_tokens=int(a["budget_tokens"])
|
|
2588
|
+
if a.get("budget_tokens") is not None else 1200,
|
|
2546
2589
|
include_state=bool(a.get("include_state", True)))
|
|
2547
2590
|
if act == "compact":
|
|
2548
2591
|
if a.get("note") and principal is not None \
|
|
@@ -2594,7 +2637,7 @@ def _skip_dirs_report(stats, limit=20):
|
|
|
2594
2637
|
return out
|
|
2595
2638
|
|
|
2596
2639
|
|
|
2597
|
-
#
|
|
2640
|
+
# 生效条件:先经 check_path_root(a.get("path"), "MDCG_INGEST_ROOT", "ingest")(env 未设置或 path 空时放行,越界抛 PermissionError);act=(a.get("action") or "stat").strip().lower();act 属 ("file","dir","jsonl") 且 principal 非 None 且其 can_write 为假时先 require_admin(f"ingest_{act}"),随后以 action=act 及各透传参数调 sources.run 并返回。
|
|
2598
2641
|
def _ingest_call(cg, a):
|
|
2599
2642
|
"""文件摄取分派(P0):file / dir / jsonl / stat。
|
|
2600
2643
|
|
|
@@ -2602,7 +2645,11 @@ def _ingest_call(cg, a):
|
|
|
2602
2645
|
支持 dry_run 预演(对应计划「可预演」要求)。
|
|
2603
2646
|
"""
|
|
2604
2647
|
from . import sources
|
|
2648
|
+
from .security import check_path_root
|
|
2605
2649
|
act = (a.get("action") or "stat").strip().lower()
|
|
2650
|
+
# P1-4(批次 26):ingest 的 path 是模型可控输入——根白名单约束
|
|
2651
|
+
# (MDCG_INGEST_ROOT 设置时越界即拒;未设置保持现状)
|
|
2652
|
+
check_path_root(a.get("path"), "MDCG_INGEST_ROOT", "ingest")
|
|
2606
2653
|
principal = getattr(cg, "principal", None)
|
|
2607
2654
|
if act in ("file", "dir", "jsonl") and principal is not None \
|
|
2608
2655
|
and not getattr(principal, "can_write", False):
|
|
@@ -2615,17 +2662,20 @@ def _ingest_call(cg, a):
|
|
|
2615
2662
|
max_events=a.get("max_events"))
|
|
2616
2663
|
|
|
2617
2664
|
|
|
2618
|
-
#
|
|
2665
|
+
# 生效条件:先经 require_admin("export_{act}")(principal 非 None 时)与 check_path_root(a.get("out"), "MDCG_EXPORT_ROOT", "export")(env 未设置或 out 空时放行,越界抛 PermissionError);act=(a.get("action") or "stat").strip().lower();principal 非 None 时一律先 require_admin(f"export_{act}"),随后以 include_content=True if a.get("include_content") is None else bool(a.get("include_content")) 等参数调 _ex.run 并返回。
|
|
2619
2666
|
def _export_call(cg, a):
|
|
2620
2667
|
"""全库导出(P0):graph / nodes / slice / stat。
|
|
2621
2668
|
|
|
2622
2669
|
导出整库属**管理操作** → 一律 require_admin(designer 专属)。
|
|
2623
2670
|
"""
|
|
2624
2671
|
from . import export as _ex
|
|
2672
|
+
from .security import check_path_root
|
|
2625
2673
|
act = (a.get("action") or "stat").strip().lower()
|
|
2626
2674
|
principal = getattr(cg, "principal", None)
|
|
2627
2675
|
if principal is not None:
|
|
2628
2676
|
principal.require_admin(f"export_{act}")
|
|
2677
|
+
# P1-4(批次 26):export 的 out 是写路径——根白名单约束
|
|
2678
|
+
check_path_root(a.get("out"), "MDCG_EXPORT_ROOT", "export")
|
|
2629
2679
|
inc = a.get("include_content")
|
|
2630
2680
|
return _ex.run(
|
|
2631
2681
|
cg, action=act, out=a.get("out"), ids=a.get("ids"),
|
|
@@ -2636,10 +2686,10 @@ def _export_call(cg, a):
|
|
|
2636
2686
|
|
|
2637
2687
|
# 生效条件:act=(a.get("action") or "stat").strip().lower(),apply=bool(a.get("apply")),mode=str(a.get("mode") or "").strip().lower();principal 非 None 时对 act=="rollback"、act in ("importance","separate") 且 apply、act=="longterm" 且 apply、act=="prefeed" 且 a.get("write") 真且无写权、act in ("backfill","cap","exempt") 且 apply、act in ("backfill_rollback","cap_rollback","exempt_rollback")、act=="vision_evidence" 且 apply、act=="vision_evidence_rollback"、act=="refine" 且 apply 分别 require_admin;随后 act=="longterm" 且 mode in ("list","ls","show","read") 时仅以 action/mode/limit/snapshot_id 调 cg.maintain,否则以全部参数调 cg.maintain。
|
|
2638
2688
|
def _maintain_call(cg, a):
|
|
2639
|
-
"""记忆维护(P1):importance / longterm / prefeed / separate / rollback / stat / propagate。
|
|
2689
|
+
"""记忆维护(P1):importance / longterm / prefeed / separate / rollback / stat / propagate / reload。
|
|
2640
2690
|
|
|
2641
2691
|
权限**按 action 分档**(比整 op 收窄更贴合语义):
|
|
2642
|
-
· 只读(stat / history / longterm mode=list|show
|
|
2692
|
+
· 只读(stat / history / longterm mode=list|show / reload 读面代际刷新)→ 不额外拦截;
|
|
2643
2693
|
· 写入侧闸门(prefeed write=True)→ 需 can_write;
|
|
2644
2694
|
· 批量改写(importance / separate / longterm 的 apply,以及 rollback 本身
|
|
2645
2695
|
即反向写入、无 dry-run 语义)→ require_admin。
|
|
@@ -2851,6 +2901,17 @@ def _ref_call(cg, a):
|
|
|
2851
2901
|
if not ref:
|
|
2852
2902
|
return {"ok": False,
|
|
2853
2903
|
"error": "该节点没有 code_ref/doc_ref(不是索引节点)"}
|
|
2904
|
+
# P1-X(两份独立报告合并):ref 回读是任意文件读原语——
|
|
2905
|
+
# 自报 root、inline ref 自带的 root/path 均模型可控,probe_ref 直接
|
|
2906
|
+
# os.path.join 后 open 全文回读(绝对 path 丢弃 root、'..' 上跳同漏)。
|
|
2907
|
+
# 按最终将打开的完整路径过根白名单:env 未设置=放开(部署开关,与
|
|
2908
|
+
# P1-4 的 ingest/export/link 同语义);设置了=realpath 落根内否则拒
|
|
2909
|
+
# (fail-closed),上跳与绝对路径在 realpath 归一后自然涵盖。
|
|
2910
|
+
from .security import check_path_root
|
|
2911
|
+
check_path_root(
|
|
2912
|
+
os.path.join(a.get("root") or ref.get("root") or "",
|
|
2913
|
+
ref.get("path") or ""),
|
|
2914
|
+
"MDCG_INGEST_ROOT", "ref")
|
|
2854
2915
|
out = refindex.read_ref(ref, root=a.get("root"), ref_kind=ref_kind)
|
|
2855
2916
|
out["node_id"] = nid or None
|
|
2856
2917
|
return out
|
|
@@ -2942,17 +3003,17 @@ def call_tool(cg, name, args):
|
|
|
2942
3003
|
scope = narrowed_principal(p, unit)
|
|
2943
3004
|
except TokenError as e:
|
|
2944
3005
|
return {"ok": False, "error": f"as_unit 非法:{e}"}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
if scope is p:
|
|
3006
|
+
# 请求级 session 只做**归因**(cg.session,写入归属/_attribution 取它),
|
|
3007
|
+
# **不**改 principal.session——绑定档(private/secret)的读授权锚定连接级
|
|
3008
|
+
# 身份(issue #35 会话隔离定稿:调用方自报的会话不得成为看他人 private
|
|
3009
|
+
# 的授权;与上方「session 不参与任何 ops 裁决」的原设计意图一致)。
|
|
3010
|
+
if scope is p and not sess:
|
|
2950
3011
|
return _dispatch(cg, name, a)
|
|
2951
3012
|
saved_p, saved_s = cg.principal, getattr(cg, "session", None)
|
|
2952
3013
|
had_s = hasattr(cg, "session")
|
|
2953
3014
|
cg.principal = scope # 单线程 stdin 循环:无并发竞争
|
|
2954
3015
|
if had_s:
|
|
2955
|
-
cg.session = scope.session
|
|
3016
|
+
cg.session = sess or scope.session
|
|
2956
3017
|
try:
|
|
2957
3018
|
return _dispatch(cg, name, a)
|
|
2958
3019
|
finally:
|
|
@@ -2961,9 +3022,45 @@ def call_tool(cg, name, args):
|
|
|
2961
3022
|
cg.session = saved_s
|
|
2962
3023
|
|
|
2963
3024
|
|
|
2964
|
-
#
|
|
3025
|
+
# P1-1(批次 24,外部审查报告):mdcg_* 工具的 op 要求映射——此前 require_op
|
|
3026
|
+
# 只在 _cg_dispatch(cg 工具)内生效,mdcg_* 分支直接落库层(只验
|
|
3027
|
+
# can_write/层/密级,不验 ops_allow)→「--ops-allow read」的只读令牌仍可经
|
|
3028
|
+
# mdcg_remember 写入,权限收窄形同虚设。映射用令牌签发域内的既有 op 词
|
|
3029
|
+
# (read/write/verify——tokens.ROLE_SPECS 全角色都含 read,写类角色含
|
|
3030
|
+
# write),ops_allow=None(不限)令牌零影响。
|
|
3031
|
+
_MDCG_OP_REQUIRE = {
|
|
3032
|
+
# 写面
|
|
3033
|
+
"mdcg_remember": "write", "mdcg_reflect": "write",
|
|
3034
|
+
"mdcg_flywheel": "write", "mdcg_mine_fix_pairs": "write",
|
|
3035
|
+
"mdcg_rejected": "write", "mdcg_unresolved": "write",
|
|
3036
|
+
"mdcg_propose": "write", "mdcg_review_decide": "write",
|
|
3037
|
+
"mdcg_forget": "write", "mdcg_restore": "write",
|
|
3038
|
+
"mdcg_protect": "write", "mdcg_ingest": "write",
|
|
3039
|
+
"mdcg_consistency": "write",
|
|
3040
|
+
# 读面
|
|
3041
|
+
"mdcg_recall": "read", "mdcg_search": "read", "mdcg_get": "read",
|
|
3042
|
+
"mdcg_review_list": "read", "mdcg_review_records": "read",
|
|
3043
|
+
"mdcg_forgetting_history": "read", "mdcg_identity": "read",
|
|
3044
|
+
"mdcg_metacognition": "read", "mdcg_self_state": "read",
|
|
3045
|
+
"mdcg_predict": "read", "mdcg_causal": "read",
|
|
3046
|
+
"mdcg_evolution": "read", "mdcg_health": "read",
|
|
3047
|
+
"mdcg_whoami": "read", "mdcg_watermarks": "read",
|
|
3048
|
+
"mdcg_whitebox": "read",
|
|
3049
|
+
# 裁决面(仅高权角色域含 verify)
|
|
3050
|
+
"mdcg_verify": "verify",
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
|
|
3054
|
+
# 生效条件:name 为已注册工具名之一(cg / stg / mdcg_whitebox / mdcg_service_info / mdcg_remember 等);name 属 _MDCG_OP_REQUIRE 且 cg.principal 具 require_op 属性时先 require_op(越权抛 AccessDenied),principal 为 None 或无该方法时跳过;cg 走 _cg_call、stg 走 _stg_call、whitebox 走 _whitebox_call;未识别的 name 返回含 error 的响应字典而不抛异常,进程不因此中断;
|
|
2965
3055
|
def _dispatch(cg, name, args):
|
|
2966
3056
|
a = args or {}
|
|
3057
|
+
# P1-1:mdcg_* 面的角色作用域闸(与 cg 工具的 require_op 同一语义——
|
|
3058
|
+
# ops_allow=None 令牌不受影响;受限令牌越权即 AccessDenied)
|
|
3059
|
+
_need_op = _MDCG_OP_REQUIRE.get(name)
|
|
3060
|
+
if _need_op:
|
|
3061
|
+
_p = getattr(cg, "principal", None)
|
|
3062
|
+
if _p is not None and hasattr(_p, "require_op"):
|
|
3063
|
+
_p.require_op(_need_op)
|
|
2967
3064
|
if name == "cg":
|
|
2968
3065
|
return _cg_call(cg, a)
|
|
2969
3066
|
if name == "stg":
|
|
@@ -3037,7 +3134,9 @@ def _dispatch(cg, name, args):
|
|
|
3037
3134
|
# 直接引用 mdcos.DEFAULT_MAX_ITEM_TOKENS 会 NameError —— 故此处按需导入)。
|
|
3038
3135
|
from .mdcos import DEFAULT_MAX_ITEM_TOKENS as _DEFAULT_MAX_ITEM
|
|
3039
3136
|
return cg.recall(a.get("query", ""),
|
|
3040
|
-
|
|
3137
|
+
# `is not None`:显式 0 不再被吞成默认 1200(2026-09-24 修复)
|
|
3138
|
+
budget_tokens=int(a["budget_tokens"])
|
|
3139
|
+
if a.get("budget_tokens") is not None else 1200,
|
|
3041
3140
|
max_item_tokens=int(a["max_item_tokens"])
|
|
3042
3141
|
if a.get("max_item_tokens") is not None
|
|
3043
3142
|
else _DEFAULT_MAX_ITEM,
|
|
@@ -3066,15 +3165,24 @@ def _dispatch(cg, name, args):
|
|
|
3066
3165
|
for n, s, q in res]}
|
|
3067
3166
|
|
|
3068
3167
|
if name == "mdcg_get":
|
|
3069
|
-
|
|
3070
|
-
|
|
3168
|
+
_nid = a.get("node_id", "")
|
|
3169
|
+
node = cg.get(_nid)
|
|
3170
|
+
if not node:
|
|
3171
|
+
# 裸 null 让调用方无从区分「id 不存在」与「密级不可读」(2026-09-24 修复)
|
|
3172
|
+
return {"ok": False, "error": "not_found", "node_id": _nid,
|
|
3173
|
+
"readable_sensitivities": _readable_sensitivities(cg),
|
|
3174
|
+
"hint": "id 不存在(或已被清退/证伪移入 rejected),"
|
|
3175
|
+
"或该节点密级高于当前身份可读——whoami 可查 clearance"}
|
|
3176
|
+
return _node_view(node, offset=int(a.get("offset") or 0))
|
|
3071
3177
|
|
|
3072
3178
|
if name == "mdcg_reflect":
|
|
3073
3179
|
res, _ = cg.search(a.get("query", ""), k=int(a.get("k") or 10), record=False)
|
|
3074
3180
|
return cg.reflect(a.get("query", ""), res, a.get("feedback"))
|
|
3075
3181
|
|
|
3076
3182
|
if name == "mdcg_verify":
|
|
3077
|
-
return cg.verify(a.get("node_id", ""), a.get("evidence", ""),
|
|
3183
|
+
return cg.verify(a.get("node_id", ""), a.get("evidence", ""),
|
|
3184
|
+
a.get("verdict", ""),
|
|
3185
|
+
override=bool(a.get("override")))
|
|
3078
3186
|
|
|
3079
3187
|
if name == "mdcg_flywheel":
|
|
3080
3188
|
return cg.flywheel_step(a.get("error_report") or {})
|
|
@@ -3112,7 +3220,8 @@ def _dispatch(cg, name, args):
|
|
|
3112
3220
|
return cg.review_decide(a.get("pid", ""), a.get("decision", ""),
|
|
3113
3221
|
edits=a.get("edits"), merge_into=a.get("merge_into"),
|
|
3114
3222
|
reason=a.get("reason", ""),
|
|
3115
|
-
redteam=a.get("redteam"), issues=a.get("issues")
|
|
3223
|
+
redteam=a.get("redteam"), issues=a.get("issues"),
|
|
3224
|
+
override=bool(a.get("override")))
|
|
3116
3225
|
|
|
3117
3226
|
if name == "mdcg_review_records":
|
|
3118
3227
|
nid = a.get("node_id")
|
|
@@ -3163,6 +3272,12 @@ def _dispatch(cg, name, args):
|
|
|
3163
3272
|
if name == "mdcg_ingest":
|
|
3164
3273
|
from .sources import DSHSessionSource, JsonlSource, Ingestor
|
|
3165
3274
|
src_arg = (a.get("source") or "auto").strip()
|
|
3275
|
+
# P1-4(批次 26):报告点名「mdcg_ingest 完全没有权限闸」——权限闸
|
|
3276
|
+
# 已由批次 24 _MDCG_OP_REQUIRE(write)补上,此处补路径根约束;
|
|
3277
|
+
# auto(自动发现 ~/.dsh/sessions)不经路径参数
|
|
3278
|
+
from .security import check_path_root
|
|
3279
|
+
if src_arg != "auto":
|
|
3280
|
+
check_path_root(src_arg, "MDCG_INGEST_ROOT", "mdcg_ingest")
|
|
3166
3281
|
ing = Ingestor(cg)
|
|
3167
3282
|
picked, cands = None, []
|
|
3168
3283
|
if src_arg == "auto":
|
|
@@ -3342,7 +3457,7 @@ def _apply_attribution(p):
|
|
|
3342
3457
|
p.unit = unit
|
|
3343
3458
|
|
|
3344
3459
|
|
|
3345
|
-
# 生效条件:环境变量 MDCG_TOKEN 去空白后非空时经 verify_token(token, tenant=os.environ.get("MDCG_TENANT")) 构造——抛 TokenError 则返回 (None, f"令牌校验失败:{e}"),成功则返回 (_attach_theory(p), None);否则 MDCG_LEGACY_ENV_AUTH 值为 "1"/"true"/"True" 时按 MDCG_CAN_ADMIN/MDCG_CAN_WRITE
|
|
3460
|
+
# 生效条件:环境变量 MDCG_TOKEN 去空白后非空时经 verify_token(token, tenant=os.environ.get("MDCG_TENANT")) 构造——抛 TokenError 则返回 (None, f"令牌校验失败:{e}"),成功则返回 (_attach_theory(p), None);否则 MDCG_LEGACY_ENV_AUTH 值为 "1"/"true"/"True" 时按 MDCG_CAN_ADMIN/MDCG_CAN_WRITE 落角色(P1-5 批次 26:admin 需 MDCG_CAN_ADMIN 与 MDCG_LEGACY_ENV_ADMIN 同时为真,只设前者时降级 recorder 并写 stderr 提示),两者都不满足时构造 can_write=False、can_admin=False、auth_mode="anonymous" 的 guest 身份,后两条路径同样返回 (_attach_theory(p), None);
|
|
3346
3461
|
def _build_principal():
|
|
3347
3462
|
"""构造 Principal(令牌优先,fail-closed)。返回 (principal, error)。
|
|
3348
3463
|
|
|
@@ -3364,11 +3479,33 @@ def _build_principal():
|
|
|
3364
3479
|
p = verify_token(token, tenant=os.environ.get("MDCG_TENANT"))
|
|
3365
3480
|
except TokenError as e:
|
|
3366
3481
|
return None, f"令牌校验失败:{e}"
|
|
3482
|
+
# 密级口径冲突必须开口(2026-09-24 修复):令牌优先时 MDCG_CLEARANCE
|
|
3483
|
+
# **完全不参与**(令牌里冻结的 clearance 说了算),而插件侧
|
|
3484
|
+
# config.mdcg.clearance 默认 'private'——配置写 private、实际 internal
|
|
3485
|
+
# 会让 private/secret 节点静默读不到(本机实测:readable=[public,internal])。
|
|
3486
|
+
_want = (os.environ.get("MDCG_CLEARANCE") or "").strip()
|
|
3487
|
+
if _want and _want != (p.clearance or ""):
|
|
3488
|
+
sys.stderr.write(
|
|
3489
|
+
f"[mdcg-mcp] ⚠ 密级配置被令牌覆盖:MDCG_CLEARANCE={_want} 但令牌"
|
|
3490
|
+
f" clearance={p.clearance}(令牌优先,实际可读密级以令牌为准)。"
|
|
3491
|
+
f"要让配置生效:用 --clearance {_want} 重新签发令牌,"
|
|
3492
|
+
f"或删除 MDCG_TOKEN 走 legacy/env 身份。\n")
|
|
3367
3493
|
_apply_attribution(p)
|
|
3368
3494
|
return _attach_theory(p), None
|
|
3369
3495
|
|
|
3370
3496
|
if os.environ.get("MDCG_LEGACY_ENV_AUTH", "0") in ("1", "true", "True"):
|
|
3371
|
-
|
|
3497
|
+
# P1-5(批次 26,外部审查报告):legacy env 直连身份不再能给 admin——
|
|
3498
|
+
# 「环境变量可达即 designer/can_admin」= 配置即提权后门。保留兼容但
|
|
3499
|
+
# admin 需**显式二次开关** MDCG_LEGACY_ENV_ADMIN=1;只设 MDCG_CAN_ADMIN
|
|
3500
|
+
# 时降级 recorder 并 stderr 提示(不静默)。
|
|
3501
|
+
_want_admin = os.environ.get("MDCG_CAN_ADMIN", "0") in ("1", "true", "True")
|
|
3502
|
+
can_admin = _want_admin and os.environ.get(
|
|
3503
|
+
"MDCG_LEGACY_ENV_ADMIN", "0") in ("1", "true", "True")
|
|
3504
|
+
if _want_admin and not can_admin:
|
|
3505
|
+
sys.stderr.write(
|
|
3506
|
+
"[mdcg-mcp] ⚠ MDCG_CAN_ADMIN=1 在 legacy env 身份下已不生效:"
|
|
3507
|
+
"管理员需要二次开关 MDCG_LEGACY_ENV_ADMIN=1(P1-5 收紧,防 env "
|
|
3508
|
+
"注入自授 admin)。当前降级为 recorder。\n")
|
|
3372
3509
|
can_write = os.environ.get("MDCG_CAN_WRITE", "1") not in ("0", "false", "False")
|
|
3373
3510
|
role = "designer" if can_admin else ("recorder" if can_write else "output")
|
|
3374
3511
|
spec = role_spec(role)
|
|
@@ -3407,8 +3544,96 @@ def _attach_theory(p):
|
|
|
3407
3544
|
|
|
3408
3545
|
|
|
3409
3546
|
# 生效条件:环境变量 MDCG_ROOT 非空且其 os.path.abspath 规范化后的 basename 小写不以 "_md_cg_" 开头、且 _build_principal() 返回的 err 为空时,构造 MdCGSecure(root, principal=principal, autoflush=1) 并对 sys.stdin 逐行 method 分派(initialize 回 protocolVersion=PROTOCOL_VERSION 与 SERVER_NAME/SERVER_VERSION,tools/list 回 tools_for_surface(),tools/call 经 call_tool 后回 content,shutdown 跳出循环),遍历结束后调用 sustain.stop_all() 与 cg.close() 并返回 0;MDCG_ROOT 为空或 basename 命中 "_md_cg_" 前缀返回 2,令牌校验失败返回 3;
|
|
3547
|
+
# 生效条件:env 缺省取 os.environ;返回 (root, err)——MDCG_TENANT 已登记(登记表路径可经 MDCG_TENANT_REGISTRY 覆盖,缺省 ~/.mdcg/_tenants.json)时 root=登记根,与显式 MDCG_ROOT 冲突返回 (None, 冲突说明);未登记租户回落 MDCG_ROOT;两者皆空返回 (None, None);登记表损坏/不可读(TenantRegistry.load_error 留痕或读取抛异常,N124 2026-09-25)时向 stderr 写「登记根绑定失效回落 MDCG_ROOT」告警后按未登记同形回落(零闸变,只开口)。
|
|
3548
|
+
def _resolve_root(env=None):
|
|
3549
|
+
"""租户 × 认知图根解析(issue #35 接线,独立函数供守卫测试)。
|
|
3550
|
+
|
|
3551
|
+
口径:MDCG_TENANT 已登记 → 登记根;与显式 MDCG_ROOT 不一致 → 冲突
|
|
3552
|
+
(fail-closed,由调用方拒启);未登记租户 → 维持 MDCG_ROOT(零变更)。
|
|
3553
|
+
登记表损坏/不可读 → 同未登记回落,但必须开口(N124):静默回落会让
|
|
3554
|
+
租户根物理隔离绑定无声消失、写入落进公共根,且下方冲突 fail-closed
|
|
3555
|
+
检测连带失效。
|
|
3556
|
+
"""
|
|
3557
|
+
env = env if env is not None else os.environ
|
|
3558
|
+
root = env.get("MDCG_ROOT")
|
|
3559
|
+
tenant = env.get("MDCG_TENANT")
|
|
3560
|
+
if tenant:
|
|
3561
|
+
t_root = None
|
|
3562
|
+
reg_err = ""
|
|
3563
|
+
try:
|
|
3564
|
+
from .security import TenantRegistry
|
|
3565
|
+
reg = TenantRegistry(env.get("MDCG_TENANT_REGISTRY") or None)
|
|
3566
|
+
t_root = reg.root_of(tenant)
|
|
3567
|
+
reg_err = getattr(reg, "load_error", "") or ""
|
|
3568
|
+
except Exception as e: # noqa: BLE001
|
|
3569
|
+
reg_err = f"{type(e).__name__}: {e}"
|
|
3570
|
+
t_root = None
|
|
3571
|
+
if reg_err:
|
|
3572
|
+
# N124(2026-09-25 止血):登记表损坏/不可读时隔离绑定无声消失,
|
|
3573
|
+
# 对照 MDCG_CLEARANCE 覆盖告警同款口径——零闸变、只开口。
|
|
3574
|
+
sys.stderr.write(
|
|
3575
|
+
f"[mdcg-mcp] ⚠ 租户登记表损坏/不可读({reg_err}):"
|
|
3576
|
+
f"MDCG_TENANT={tenant} 的登记根绑定失效,本次回落 "
|
|
3577
|
+
f"MDCG_ROOT={root or '(未设)'};请修复登记表"
|
|
3578
|
+
f"(MDCG_TENANT_REGISTRY,缺省 ~/.mdcg/_tenants.json)。\n")
|
|
3579
|
+
if t_root:
|
|
3580
|
+
if root and os.path.abspath(root) != os.path.abspath(t_root):
|
|
3581
|
+
return None, (f"MDCG_TENANT={tenant} 已登记根 {t_root},"
|
|
3582
|
+
f"与显式 MDCG_ROOT={root} 不一致(fail-closed)")
|
|
3583
|
+
root = t_root
|
|
3584
|
+
return root, None
|
|
3585
|
+
|
|
3586
|
+
|
|
3587
|
+
# 生效条件:sys.stdin/stdout/stderr 三个流逐个 reconfigure(encoding="utf-8", errors="replace"),
|
|
3588
|
+
# 流不支持 reconfigure(AttributeError,如被替换为非 TextIO 对象)或取值非法(ValueError)时静默跳过该流,无返回值。
|
|
3589
|
+
def _force_utf8_stdio():
|
|
3590
|
+
"""进程内强制 stdio 三流 = UTF-8(issue #39 纵深防线)。
|
|
3591
|
+
|
|
3592
|
+
MCP 协议是 UTF-8 JSON,但 Windows 控制台默认代码页(如 CP936/GBK)会让
|
|
3593
|
+
stdio 管道跟随 locale——宿主按 UTF-8 发来的中文参数被按 GBK 解码:strict
|
|
3594
|
+
下读侧直接 UnicodeDecodeError,surrogateescape 下解出代理对(\\udcXX),
|
|
3595
|
+
``_j()``(ensure_ascii=False)按 UTF-8 编码时即抛 ``UnicodeEncodeError:
|
|
3596
|
+
surrogates not allowed``,整条请求失败(写英文正常,故用户难自查)。
|
|
3597
|
+
桥层已在 ``src/lib/mdcg_client.ts`` 的 ``mdcgChildEnv()`` 注入
|
|
3598
|
+
``PYTHONUTF8=1``,但只覆盖 DSH 桥路径——Claude Code / code CLI 直接
|
|
3599
|
+
mcp.json 接入不经桥,仍踩(issue #39)。进程内 reconfigure 是对桥层
|
|
3600
|
+
防线的纵深补位;``errors="replace"`` 保证坏字节最多丢字符、不炸整条请求。
|
|
3601
|
+
必须在 main() 首行调用:早于一切 stderr 中文写与 stdin 读取。
|
|
3602
|
+
"""
|
|
3603
|
+
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
3604
|
+
try:
|
|
3605
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
3606
|
+
except (AttributeError, ValueError):
|
|
3607
|
+
pass
|
|
3608
|
+
|
|
3609
|
+
|
|
3610
|
+
# 生效条件:_force_utf8_stdio() 先执行(stdio 三流强制 UTF-8,issue #39);随后对 mdcg_root()/aux_root() 各探一次,抛 ValueError(Windows 保留设备名末段被 GetFullPathNameW 吞成设备路径,datapath._abs_host_path 守卫)时向 stderr 写一行含原始消息的告警并返回 2;此后 _resolve_root() 返回 err 非空时向 stderr 写冲突说明并返回 2;root 为空写缺少 MDCG_ROOT 并返回 2;root 的 basename 小写以 _md_cg_ 开头返回 2,令牌校验失败返回 3;其余构造 MdCGSecure 并进入 stdin 分派循环。
|
|
3410
3611
|
def main():
|
|
3411
|
-
|
|
3612
|
+
_force_utf8_stdio()
|
|
3613
|
+
# 启动早期承接受理(Windows 保留设备名守卫):MDCG_ROOT/MDCG_AUX_ROOT/
|
|
3614
|
+
# MDCG_DATA_ROOT 等覆盖值末段若是保留设备名(aux/con/nul/com1-9/lpt1-9…),
|
|
3615
|
+
# ntpath.abspath 经 GetFullPathNameW 会把整个路径吞成设备命名空间形态
|
|
3616
|
+
# (例 D:\sandbox\aux → \\.\aux),datapath._abs_host_path 即抛 ValueError。
|
|
3617
|
+
# 此处先探一次 root/aux 解析,命中即拒绝启动(对照下方 `_md_cg_` 前缀守卫
|
|
3618
|
+
# 先例「启动即拒绝,不带病运行」)——否则首个 aux 常量面(crypto/links/
|
|
3619
|
+
# signer/theory/tokens/evidence 的模块级 aux_root())会在下方 import 处
|
|
3620
|
+
# 裸崩 traceback,密钥/令牌/信任面被指到不存在的设备路径。
|
|
3621
|
+
try:
|
|
3622
|
+
from .datapath import mdcg_root as _probe_root
|
|
3623
|
+
from .datapath import aux_root as _probe_aux
|
|
3624
|
+
_probe_root()
|
|
3625
|
+
_probe_aux()
|
|
3626
|
+
except ValueError as _ve:
|
|
3627
|
+
sys.stderr.write(f"[mdcg-mcp] {_ve}\n"
|
|
3628
|
+
"[mdcg-mcp] 拒绝启动(fail-closed):请把上述覆盖键"
|
|
3629
|
+
"改指向末段不含保留设备名的普通目录。\n")
|
|
3630
|
+
return 2
|
|
3631
|
+
root, _terr = _resolve_root()
|
|
3632
|
+
if _terr:
|
|
3633
|
+
sys.stderr.write(f"[mdcg-mcp] 租户根冲突:{_terr}。"
|
|
3634
|
+
"请二选一:清空 MDCG_ROOT 走租户登记根,或从登记表"
|
|
3635
|
+
"(~/.mdcg/_tenants.json)移除该租户。\n")
|
|
3636
|
+
return 2
|
|
3412
3637
|
if not root:
|
|
3413
3638
|
sys.stderr.write("[mdcg-mcp] 缺少 MDCG_ROOT 环境变量\n")
|
|
3414
3639
|
return 2
|
|
@@ -3444,6 +3669,15 @@ def main():
|
|
|
3444
3669
|
except Exception as _mig_exc: # 迁移故障不得影响服务可用性
|
|
3445
3670
|
sys.stderr.write("[mdcg-mcp] 数据面迁移检查失败(不阻塞启动): %r\n"
|
|
3446
3671
|
% (_mig_exc,))
|
|
3672
|
+
# 两个根一并留痕(2026-09-24):记忆真源与「身份/凭据根」**有意分离**
|
|
3673
|
+
# (身份不随认知图迁移),但分居两处必须可见——历史事故多为「以为在一处」。
|
|
3674
|
+
try:
|
|
3675
|
+
from .datapath import aux_root as _aux_root
|
|
3676
|
+
sys.stderr.write(
|
|
3677
|
+
"[mdcg-mcp] 路径:记忆真源 root=%s · 身份/凭据根 aux=%s"
|
|
3678
|
+
"(aux 可用 MDCG_AUX_ROOT 覆盖)\n" % (root, _aux_root()))
|
|
3679
|
+
except Exception: # 仅日志:任何失败都不影响启动
|
|
3680
|
+
pass
|
|
3447
3681
|
principal, err = _build_principal()
|
|
3448
3682
|
if err:
|
|
3449
3683
|
sys.stderr.write(
|
|
@@ -3489,6 +3723,17 @@ def main():
|
|
|
3489
3723
|
except Exception as _exc: # 对账故障不得影响服务可用性
|
|
3490
3724
|
sys.stderr.write("[mdcg-mcp] 两段式对账失败(不阻塞启动): %r\n"
|
|
3491
3725
|
% (_exc,))
|
|
3726
|
+
# ③' 启动对账 reconcile v0(v0.3 T9 自稳定定理落地,落地清单 P0-3):两段式
|
|
3727
|
+
# 清账只覆盖「有意图无结果的写入」,这里补全量 diff——索引(派生态)vs 盘面
|
|
3728
|
+
# 真源:多/缺索引条目与内容哈希漂移三类,以盘面为真源**增量**修复索引面;
|
|
3729
|
+
# 真源自身损坏只告警留痕不自动改写(T9 边界:结构合法≠内容正确)。告警与
|
|
3730
|
+
# 留痕由模块发声(diff 非空才输出,干净库零噪声);MDCG_RECONCILE=0 关闭
|
|
3731
|
+
# (缺省开,开关真源 reconcile.enabled)。失败不阻塞启动,与③同风格。
|
|
3732
|
+
try:
|
|
3733
|
+
cg.reconcile_state()
|
|
3734
|
+
except Exception as _exc: # 对账故障不得影响服务可用性
|
|
3735
|
+
sys.stderr.write("[mdcg-mcp] 启动对账 v0 失败(不阻塞启动): %r\n"
|
|
3736
|
+
% (_exc,))
|
|
3492
3737
|
# 进程自报(诊断设施,fail-safe):把「md_cg 实际加载源 + render 契约代际」
|
|
3493
3738
|
# 落成 `<tempdir>/md_cg_servers/<pid>.json`,供 `scripts/mdcg_stale_servers.py`
|
|
3494
3739
|
# 机械判定**活进程代际**。根因(第4条取证):原先只能比「进程启动时间 vs
|
|
@@ -3542,8 +3787,14 @@ def main():
|
|
|
3542
3787
|
_reply(rid, {"content": [{"type": "text", "text": _j(out)}],
|
|
3543
3788
|
"isError": False})
|
|
3544
3789
|
except Exception as exc: # noqa: BLE001 —— 工具错误以 MCP 结果返回
|
|
3790
|
+
# issue #34:失败路径必须带「怎么办」——AccessDenied 的 hint
|
|
3791
|
+
# (guest 配凭据 / 令牌补授权 / 过期重签)随结构化错误透出。
|
|
3792
|
+
err = {"error": f"{type(exc).__name__}: {exc}"}
|
|
3793
|
+
_hint = getattr(exc, "hint", None)
|
|
3794
|
+
if _hint:
|
|
3795
|
+
err["hint"] = _hint
|
|
3545
3796
|
_reply(rid, {"content": [{"type": "text",
|
|
3546
|
-
"text": _j(
|
|
3797
|
+
"text": _j(err)}],
|
|
3547
3798
|
"isError": True})
|
|
3548
3799
|
elif method == "shutdown":
|
|
3549
3800
|
_reply(rid, {})
|