@furongjun1999/dsh-memory 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +588 -552
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/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/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/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/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_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_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +20 -0
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.js +24 -2
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.d.ts +88 -0
- package/lib/init.js +302 -0
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evidence.py +585 -582
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/fsutil.py +454 -8
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4152 -3818
- package/md_cg/mdcg.py +4208 -3462
- package/md_cg/mdcos.py +4491 -4137
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/review_cli.py +49 -4
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1299 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +166 -0
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +516 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +8 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/cli.ts +65 -0
- package/src/hooks.ts +229 -16
- package/src/init.ts +361 -0
- package/src/lib/prompt_safety.ts +88 -1
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · node_id 类型混淆(legacy P3)守卫:非字符串 id 混型索引键 → 整库重扫崩
|
|
3
|
+
|
|
4
|
+
沿革(legacy,不新占 N 号):docs/eval/DSH端缺陷专项_v1.0.md:106/:204/:249 第 4 条
|
|
5
|
+
(§六-3「P3 相邻发现未修」)+ md_cg/test_none_id_write_guard.py:111-114 自注
|
|
6
|
+
「add(123) 属相邻缺陷(_scan_nodes/_load_index 混型键 sorted 崩),超出本守卫目标」。
|
|
7
|
+
本轮为首次第一手全链复现与修复。
|
|
8
|
+
|
|
9
|
+
缺陷(修复前):md_cg/mcp_server.py:3090 `nid = a.get("node_id") or (...)` 不做类型
|
|
10
|
+
归一;md_cg/mdcg.py:1481 `nid_s = str(node_id or "")` **只**用于白名单校验与落盘
|
|
11
|
+
路径,而索引键(:1813 `_stage(node_id, …)`)、frontmatter `id`(:1590,落盘路径
|
|
12
|
+
:1571)、分片日志 `{"id": …}` 仍写**原对象**——行号按修复后记(修复前上述四行分别
|
|
13
|
+
为 1449/1781/1558/1526,即缺陷报告口径)。于是 `add(123)` 落
|
|
14
|
+
`knowledge/orphan/123.md`、盘面 frontmatter `id: 123`(YAML int)、索引键 int 123;
|
|
15
|
+
此后任何一次全量重扫(`_scan_nodes`,md_cg/mdcg.py:1338 `sorted(nodes)`)都会在
|
|
16
|
+
「int 键 vs 其它节点的 str 键」上抛 TypeError——崩点在 `MdCGSecure.__init__`
|
|
17
|
+
(mdcg.py:966→1004→1338),早于启动对账 reconcile_state,无自愈路径 ⇒ 整库永久
|
|
18
|
+
打不开。触发面:新建库首会话 + 任一字符串 id、既有快照库 + 一份外部 .md、
|
|
19
|
+
删/缺 `_index.json`(复制/还原/新机)。
|
|
20
|
+
|
|
21
|
+
修复(两层,各司其职):
|
|
22
|
+
· 写面 fail-closed(md_cg/mdcg.py:1461 区,`add` 的 node_id 校验前):`node_id`
|
|
23
|
+
非 str 一律 ValueError 拒绝——与既有 `add(None)`/`add("")`/`add(0)` 拒绝同族
|
|
24
|
+
(那三个也是非字符串),毒不再落盘;`bool` 亦拒(True 是 int 子类,
|
|
25
|
+
`str(True)="True"` 会匹配白名单)。引擎单点,全部写面(MCP cg/mdcg_*
|
|
26
|
+
op=write、writepipe、review accept、restore、consolidate…)同源收口。
|
|
27
|
+
· 派生面自愈(`_scan_nodes` :1325 + 两处分片日志重放 :1006/:1148):索引是
|
|
28
|
+
**派生物**(可重建),键一律归一到 str——存量毒文件(旧版本写出/外部编辑)
|
|
29
|
+
与新机复制还原的库由此可自愈,不再「永久打不开」。
|
|
30
|
+
|
|
31
|
+
守卫(五组,全部临时库 + 哑主密钥/哑令牌,绝不触真实 ~/.mdcg):
|
|
32
|
+
[1] 写面契约:add(123)/add(True)/add(1.5) 被 ValueError 拒(修前放行=毒落盘);
|
|
33
|
+
add(None)/add("")/add(0) 仍拒、add("123") 正常(不误伤合法字符串 id);
|
|
34
|
+
[2] 存量毒文件(frontmatter `id: 123`):删快照重开 / 保留快照+外部 .md 两种
|
|
35
|
+
形态,修前 `sorted()` TypeError 整库打不开,修后正常且键归一为 "123";
|
|
36
|
+
[3] 分片日志 int 键:重开后索引键全为 str(潜伏毒不再进索引);
|
|
37
|
+
[4] MCP 端到端(真实 stdio 子进程):mdcg_remember({node_id:123}),修前回执
|
|
38
|
+
{"ok": true, "id": 123} 且落盘 `id: 123`;修后回结构化 error 且库仍可正常
|
|
39
|
+
打开写读(服务不崩);
|
|
40
|
+
[5] 合法路径不回归:字符串 id 的 add/get/search/rebuild/close 全绿。
|
|
41
|
+
|
|
42
|
+
运行:python -m md_cg.test_legacy_p3_node_id_type
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import json
|
|
47
|
+
import os
|
|
48
|
+
import shutil
|
|
49
|
+
import subprocess
|
|
50
|
+
import sys
|
|
51
|
+
import tempfile
|
|
52
|
+
|
|
53
|
+
from . import crypto, nodefile
|
|
54
|
+
from .mdcos import MdCGSecure
|
|
55
|
+
from .security import Principal
|
|
56
|
+
|
|
57
|
+
PASS = FAIL = 0
|
|
58
|
+
FAILS = []
|
|
59
|
+
|
|
60
|
+
DUMMY_KEK = bytes.fromhex("ab" * 32) # 哑主密钥:本进程与子进程绝不触真实 ~/.mdcg
|
|
61
|
+
POISON_ID = 123
|
|
62
|
+
POISON_BODY = "# 正文:存量毒节点(旧版本写出 / 外部编辑形态)"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def check(name, cond, detail=""):
|
|
66
|
+
global PASS, FAIL
|
|
67
|
+
if cond:
|
|
68
|
+
PASS += 1
|
|
69
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
70
|
+
else:
|
|
71
|
+
FAIL += 1
|
|
72
|
+
FAILS.append(name)
|
|
73
|
+
print(f" [FAIL] {name} · {detail}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _principal(**kw):
|
|
77
|
+
d = dict(actor="p3", clearance="secret", can_write=True, can_admin=True,
|
|
78
|
+
role="designer", auth_mode="test")
|
|
79
|
+
d.update(kw)
|
|
80
|
+
return Principal(**d)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _lib(root):
|
|
84
|
+
return MdCGSecure(root, principal=_principal(), master_key=DUMMY_KEK)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _poison_file(root, nid=POISON_ID, stem="123"):
|
|
88
|
+
"""在盘面直接落一个 frontmatter id 非字符串的节点文件(存量脏数据形态)。"""
|
|
89
|
+
d = os.path.join(root, "knowledge", "orphan")
|
|
90
|
+
os.makedirs(d, exist_ok=True)
|
|
91
|
+
p = os.path.join(d, stem + ".md")
|
|
92
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
93
|
+
f.write(nodefile.dumps({"id": nid, "layer": "knowledge",
|
|
94
|
+
"importance": 0.5, "tags": []}, POISON_BODY))
|
|
95
|
+
return p
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _open_raises(root):
|
|
99
|
+
"""构造实例;返回 (异常实例 or None, 实例 or None)。"""
|
|
100
|
+
try:
|
|
101
|
+
return None, _lib(root)
|
|
102
|
+
except Exception as e: # noqa: BLE001
|
|
103
|
+
return e, None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _keys_all_str(cg):
|
|
107
|
+
return all(isinstance(k, str) for k in (list(cg.index.get("nodes") or {})))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _Mcp:
|
|
111
|
+
"""最小 MCP stdio 客户端(测试用;协议细节同 test_p21_tokens)。"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, env):
|
|
114
|
+
self.p = subprocess.Popen(
|
|
115
|
+
[sys.executable, "-m", "md_cg.mcp_server"],
|
|
116
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
117
|
+
stderr=subprocess.PIPE, text=True, encoding="utf-8", env=env,
|
|
118
|
+
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
119
|
+
self.i = 0
|
|
120
|
+
self.send("initialize", {"protocolVersion": "2024-11-05",
|
|
121
|
+
"capabilities": {},
|
|
122
|
+
"clientInfo": {"name": "p3", "version": "1"}})
|
|
123
|
+
self.send("notifications/initialized", {}, notify=True)
|
|
124
|
+
|
|
125
|
+
def send(self, method, params=None, notify=False):
|
|
126
|
+
msg = {"jsonrpc": "2.0", "method": method}
|
|
127
|
+
if params is not None:
|
|
128
|
+
msg["params"] = params
|
|
129
|
+
if not notify:
|
|
130
|
+
self.i += 1
|
|
131
|
+
msg["id"] = self.i
|
|
132
|
+
self.p.stdin.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
133
|
+
self.p.stdin.flush()
|
|
134
|
+
if notify:
|
|
135
|
+
return None
|
|
136
|
+
line = self.p.stdout.readline()
|
|
137
|
+
if not line:
|
|
138
|
+
raise RuntimeError("MCP 无响应:"
|
|
139
|
+
+ (self.p.stderr.read() or "")[:400])
|
|
140
|
+
return json.loads(line)
|
|
141
|
+
|
|
142
|
+
def call(self, name, args):
|
|
143
|
+
r = self.send("tools/call", {"name": name, "arguments": args})
|
|
144
|
+
if "error" in r:
|
|
145
|
+
return {"error": str(r["error"])}
|
|
146
|
+
return json.loads(r["result"]["content"][0]["text"])
|
|
147
|
+
|
|
148
|
+
def __exit__(self, *a):
|
|
149
|
+
try:
|
|
150
|
+
self.send("shutdown")
|
|
151
|
+
self.p.wait(timeout=10)
|
|
152
|
+
except Exception: # noqa: BLE001
|
|
153
|
+
self.p.kill()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
roots = []
|
|
158
|
+
try:
|
|
159
|
+
tmp = tempfile.mkdtemp(prefix="p3_nodeid_")
|
|
160
|
+
roots.append(tmp)
|
|
161
|
+
|
|
162
|
+
# ---------- [1] 写面契约:非字符串 id 必须 fail-closed ----------
|
|
163
|
+
print("\n[1] 写面契约(非 str node_id)")
|
|
164
|
+
cg1 = _lib(os.path.join(tmp, "lib1"))
|
|
165
|
+
bad = []
|
|
166
|
+
for v in (123, True, 1.5):
|
|
167
|
+
try:
|
|
168
|
+
cg1.add(v, "# 正文:非字符串 id 写入")
|
|
169
|
+
bad.append("%r 竟成功" % (v,))
|
|
170
|
+
except ValueError:
|
|
171
|
+
pass
|
|
172
|
+
except Exception as e: # noqa: BLE001
|
|
173
|
+
bad.append("%r 抛 %s(非 ValueError)" % (v, type(e).__name__))
|
|
174
|
+
check("[1] 绿:add(123)/add(True)/add(1.5) 一律 ValueError 拒(修前放行=毒)",
|
|
175
|
+
not bad, ";".join(bad))
|
|
176
|
+
check("[1] 绿:索引键全为 str(毒未进内存索引)",
|
|
177
|
+
_keys_all_str(cg1),
|
|
178
|
+
str(sorted(map(repr, (cg1.index.get("nodes") or {}))))[:90])
|
|
179
|
+
still = []
|
|
180
|
+
for v in (None, "", 0):
|
|
181
|
+
try:
|
|
182
|
+
cg1.add(v, "# 正文:falsy 形态")
|
|
183
|
+
still.append("%r 竟成功" % (v,))
|
|
184
|
+
except ValueError:
|
|
185
|
+
pass
|
|
186
|
+
check("[1] 既有 falsy 契约不变(None/\"\"/0 仍拒)", not still,
|
|
187
|
+
";".join(still))
|
|
188
|
+
ok_id = cg1.add("123", "# 功能名:字符串 id\n# 正文:合法形态")
|
|
189
|
+
check("[1] 反向:add(\"123\") 字符串形态正常(不误伤)",
|
|
190
|
+
ok_id == "123", repr(ok_id))
|
|
191
|
+
# close → compact/rebuild → _scan_nodes → sorted(nodes):修前既有毒键
|
|
192
|
+
# 就在这一步炸(本会话第一手:TypeError: '<' not supported between
|
|
193
|
+
# instances of 'str' and 'float')
|
|
194
|
+
close_err = None
|
|
195
|
+
try:
|
|
196
|
+
cg1.close()
|
|
197
|
+
except Exception as e: # noqa: BLE001
|
|
198
|
+
close_err = e
|
|
199
|
+
check("[1] 绿:close()(内部 rebuild→_scan_nodes→sorted)不崩",
|
|
200
|
+
close_err is None,
|
|
201
|
+
"%s: %s" % (type(close_err).__name__, str(close_err)[:80])
|
|
202
|
+
if close_err else "")
|
|
203
|
+
|
|
204
|
+
# ---------- [2] 存量毒文件:重开/重扫不得整库打不开 ----------
|
|
205
|
+
print("\n[2] 存量毒文件(frontmatter id: 123)")
|
|
206
|
+
# 形态 A:删/缺 _index.json(复制、还原、新机)后重开
|
|
207
|
+
lib_a = os.path.join(tmp, "lib_a")
|
|
208
|
+
a = _lib(lib_a)
|
|
209
|
+
a.add("ok_node", "# 功能名:字符串节点\n# 正文:与毒节点混型的另一半")
|
|
210
|
+
a.close() # 落快照
|
|
211
|
+
_poison_file(lib_a)
|
|
212
|
+
os.remove(os.path.join(lib_a, "_index.json"))
|
|
213
|
+
ea, ca = _open_raises(lib_a)
|
|
214
|
+
check("[2] 绿:毒文件 + 缺快照 → 库仍可打开(修前 sorted TypeError)",
|
|
215
|
+
ea is None, "%s: %s" % (type(ea).__name__, str(ea)[:90]) if ea
|
|
216
|
+
else "")
|
|
217
|
+
check("[2] 绿:索引键全为 str(毒 id 归一为 \"123\")",
|
|
218
|
+
ca is not None and _keys_all_str(ca)
|
|
219
|
+
and str(POISON_ID) in (ca.index.get("nodes") or {}),
|
|
220
|
+
str(sorted((ca.index.get("nodes") or {}).keys()))[:80]
|
|
221
|
+
if ca else "库打不开")
|
|
222
|
+
if ca is not None:
|
|
223
|
+
got = ca.get(str(POISON_ID))
|
|
224
|
+
check("[2] 绿:毒节点经归一 id 可见(HTTP 语义未丢)",
|
|
225
|
+
got is not None and POISON_BODY in (got.get("content") or ""),
|
|
226
|
+
repr((got or {}).get("content"))[:60])
|
|
227
|
+
# 重扫路径也不得崩(record 修复前:_scan_nodes 的 sorted)
|
|
228
|
+
try:
|
|
229
|
+
ca.rebuild_index()
|
|
230
|
+
check("[2] 绿:rebuild_index(含 sorted)不崩", True)
|
|
231
|
+
except Exception as e: # noqa: BLE001
|
|
232
|
+
check("[2] 绿:rebuild_index(含 sorted)不崩", False,
|
|
233
|
+
"%s: %s" % (type(e).__name__, str(e)[:90]))
|
|
234
|
+
ca.close()
|
|
235
|
+
|
|
236
|
+
# 形态 B:保留快照 + 事后落一份外部 .md(指纹失配 → 全量重扫)
|
|
237
|
+
lib_b = os.path.join(tmp, "lib_b")
|
|
238
|
+
b = _lib(lib_b)
|
|
239
|
+
b.add("ok_node", "# 功能名:字符串节点\n# 正文:`b` 库另一半")
|
|
240
|
+
b.close()
|
|
241
|
+
_poison_file(lib_b) # 快照已存,事后外部落毒
|
|
242
|
+
eb, cb = _open_raises(lib_b)
|
|
243
|
+
check("[2] 绿:快照在 + 外部毒 .md(指纹失配全扫)→ 库仍可打开",
|
|
244
|
+
eb is None, "%s: %s" % (type(eb).__name__, str(eb)[:90]) if eb
|
|
245
|
+
else "")
|
|
246
|
+
if cb is not None:
|
|
247
|
+
check("[2] 绿:形态 B 索引键亦全为 str", _keys_all_str(cb))
|
|
248
|
+
cb.close()
|
|
249
|
+
|
|
250
|
+
# ---------- [3] 分片日志 int 键:潜伏毒不进索引 ----------
|
|
251
|
+
print("\n[3] 分片日志 `{\"id\": 123}` 形态")
|
|
252
|
+
lib_c = os.path.join(tmp, "lib_c")
|
|
253
|
+
c = _lib(lib_c)
|
|
254
|
+
c._dirty[POISON_ID] = {"path": "knowledge/orphan/123.md",
|
|
255
|
+
"layer": "knowledge"} # 复刻修前 _stage(123, …)
|
|
256
|
+
c.flush() # 落分片日志(不落快照)
|
|
257
|
+
ec, cc = _open_raises(lib_c)
|
|
258
|
+
check("[3] 绿:重开后索引键全为 str(修前残留 int 键)",
|
|
259
|
+
ec is None and cc is not None and _keys_all_str(cc),
|
|
260
|
+
("%s" % (sorted(map(repr, (cc.index.get("nodes") or {})))
|
|
261
|
+
if cc else ec))[:80])
|
|
262
|
+
if cc is not None:
|
|
263
|
+
cc.close()
|
|
264
|
+
|
|
265
|
+
# ---------- [4] MCP 端到端(真实 stdio 子进程) ----------
|
|
266
|
+
print("\n[4] MCP 端到端 mdcg_remember({node_id: 123})")
|
|
267
|
+
mcp_root = os.path.join(tmp, "mcp_cg")
|
|
268
|
+
aux_root = os.path.join(tmp, "auxdir") # 注意:不能叫 aux(Win32 保留设备名)
|
|
269
|
+
os.makedirs(aux_root, exist_ok=True)
|
|
270
|
+
from . import tokens as _tk
|
|
271
|
+
tf = os.path.join(tmp, "_tokens.json")
|
|
272
|
+
d = _tk.issue("designer", actor="p3-mcp", path=tf)
|
|
273
|
+
env = dict(os.environ)
|
|
274
|
+
env.update({
|
|
275
|
+
"MDCG_ROOT": mcp_root, "MDCG_TOKEN_FILE": tf,
|
|
276
|
+
"MDCG_TOKEN": d["token"],
|
|
277
|
+
"MDCG_MASTER_KEY": DUMMY_KEK.hex(), # 哑主密钥(子进程不读/不建真实密钥)
|
|
278
|
+
"MDCG_AUX_ROOT": aux_root, # 辅助根一并指向临时目录
|
|
279
|
+
"MDCG_SUSTAIN": "0", "MDCG_SUSTAIN_ENABLED": "0",
|
|
280
|
+
"PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"})
|
|
281
|
+
for k in ("MDCG_LEGACY_ENV_AUTH", "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE",
|
|
282
|
+
"MDCG_CLEARANCE", "MDCG_ACTOR", "MDCG_LEGACY_ENV_ADMIN"):
|
|
283
|
+
env.pop(k, None)
|
|
284
|
+
check("[4] 隔离声明:子进程主密钥为哑值 + 辅助根指向临时目录",
|
|
285
|
+
env.get("MDCG_MASTER_KEY") == DUMMY_KEK.hex()
|
|
286
|
+
and env.get(crypto.MASTER_ENV) is not None
|
|
287
|
+
and os.path.abspath(env["MDCG_AUX_ROOT"]).startswith(
|
|
288
|
+
os.path.abspath(tmp)))
|
|
289
|
+
m = None
|
|
290
|
+
try:
|
|
291
|
+
m = _Mcp(env)
|
|
292
|
+
except Exception as e: # noqa: BLE001
|
|
293
|
+
check("[4] MCP 子进程可启动", False,
|
|
294
|
+
"%s: %s" % (type(e).__name__, str(e)[:200]))
|
|
295
|
+
if m is not None:
|
|
296
|
+
try:
|
|
297
|
+
r = m.call("mdcg_remember",
|
|
298
|
+
{"node_id": POISON_ID,
|
|
299
|
+
"content": "# 功能名:数字 id 探针\n# 正文:毒"})
|
|
300
|
+
check("[4] 绿:MCP 回结构化 error(修前回 {\"ok\": true, \"id\": 123})",
|
|
301
|
+
isinstance(r, dict) and "error" in r
|
|
302
|
+
and "ValueError" in str(r.get("error")), str(r)[:140])
|
|
303
|
+
poisoned = os.path.join(mcp_root, "knowledge", "orphan",
|
|
304
|
+
"123.md")
|
|
305
|
+
int_id = False
|
|
306
|
+
if os.path.exists(poisoned):
|
|
307
|
+
with open(poisoned, encoding="utf-8") as f:
|
|
308
|
+
fm, _c = nodefile.loads(f.read())
|
|
309
|
+
int_id = not isinstance(fm.get("id"), str)
|
|
310
|
+
check("[4] 绿:盘面未落 int id 毒文件", not int_id,
|
|
311
|
+
"poisoned=%s" % os.path.exists(poisoned))
|
|
312
|
+
check("[4] 服务未崩(MCP 进程仍在)", m.p.poll() is None,
|
|
313
|
+
"poll=%s" % (m.p.poll(),))
|
|
314
|
+
finally:
|
|
315
|
+
m.__exit__()
|
|
316
|
+
# 端到端后果:毒节点 + 任一字符串 id 节点共存,缺快照(复制/还原/新机
|
|
317
|
+
# 形态)重开 → 全量重扫 sorted 混型崩 ⇒ 整库打不开(doc:106 定性)
|
|
318
|
+
try:
|
|
319
|
+
em, cm = _open_raises(mcp_root)
|
|
320
|
+
check("[4] 绿:该库可打开(快照可用态)", em is None,
|
|
321
|
+
"%s: %s" % (type(em).__name__, str(em)[:90]) if em else "")
|
|
322
|
+
if cm is not None:
|
|
323
|
+
nid2 = cm.add("after_probe",
|
|
324
|
+
"# 功能名:正常写入探针\n# 正文:库可用")
|
|
325
|
+
check("[4] 绿:正常字符串 id 写入可用", nid2 == "after_probe",
|
|
326
|
+
repr(nid2))
|
|
327
|
+
try:
|
|
328
|
+
cm.close()
|
|
329
|
+
except Exception: # noqa: BLE001 —— 修前 close→compact→全扫混型崩
|
|
330
|
+
pass
|
|
331
|
+
snap = os.path.join(mcp_root, "_index.json")
|
|
332
|
+
if os.path.exists(snap):
|
|
333
|
+
os.remove(snap)
|
|
334
|
+
em2, cm2 = _open_raises(mcp_root)
|
|
335
|
+
check("[4] 绿:毒+字符串共存库缺快照重开 → 不崩(修前 TypeError 整库打不开)",
|
|
336
|
+
em2 is None,
|
|
337
|
+
"%s: %s" % (type(em2).__name__, str(em2)[:90]) if em2 else "")
|
|
338
|
+
if cm2 is not None:
|
|
339
|
+
check("[4] 绿:重开后字符串节点在场",
|
|
340
|
+
cm2.get("after_probe") is not None)
|
|
341
|
+
cm2.close()
|
|
342
|
+
except Exception as e: # noqa: BLE001
|
|
343
|
+
check("[4] 端到端后果腿自身未崩", False,
|
|
344
|
+
"%s: %s" % (type(e).__name__, str(e)[:120]))
|
|
345
|
+
|
|
346
|
+
# ---------- [5] 合法路径不回归 ----------
|
|
347
|
+
print("\n[5] 合法字符串 id 路径全绿")
|
|
348
|
+
lib_d = os.path.join(tmp, "lib_d")
|
|
349
|
+
e = _lib(lib_d)
|
|
350
|
+
e.add("n_str_1", "# 功能名:字符串一\n# 正文:数字 ids 全程用字符串")
|
|
351
|
+
e.add("task_123", "# 功能名:任务\n# 正文:task_123")
|
|
352
|
+
e.flush()
|
|
353
|
+
check("[5] get/search/rebuild 正常",
|
|
354
|
+
e.get("n_str_1") is not None
|
|
355
|
+
and any(n.get("id") == "task_123" for n, _s, _q in e.search("任务")[0]))
|
|
356
|
+
e.rebuild_index()
|
|
357
|
+
check("[5] rebuild 后键仍全为 str 且节点在场",
|
|
358
|
+
_keys_all_str(e) and e.get("task_123") is not None)
|
|
359
|
+
e.close()
|
|
360
|
+
re_ = _lib(lib_d)
|
|
361
|
+
check("[5] 重开正常(无混型崩)", re_.get("n_str_1") is not None)
|
|
362
|
+
re_.close()
|
|
363
|
+
finally:
|
|
364
|
+
for r_ in roots:
|
|
365
|
+
shutil.rmtree(r_, ignore_errors=True)
|
|
366
|
+
|
|
367
|
+
print("\n" + "=" * 64)
|
|
368
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
369
|
+
if FAILS:
|
|
370
|
+
print("失败项:" + ";".join(FAILS))
|
|
371
|
+
return 0 if FAIL == 0 else 1
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
sys.exit(main())
|
package/md_cg/test_linkref.py
CHANGED
|
@@ -45,8 +45,9 @@ def _mk_cg(tmp, role="designer"):
|
|
|
45
45
|
return MdCGSecure(root, principal=p)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def _policy(tmp, required=("PASSED",), forbidden=("FORBIDDEN_WORD",)
|
|
49
|
-
|
|
48
|
+
def _policy(tmp, required=("PASSED",), forbidden=("FORBIDDEN_WORD",),
|
|
49
|
+
name="policy.json"):
|
|
50
|
+
path = os.path.join(tmp, name)
|
|
50
51
|
with open(path, "w", encoding="utf-8") as f:
|
|
51
52
|
json.dump({"forbidden": list(forbidden),
|
|
52
53
|
"required": list(required)}, f)
|
|
@@ -239,7 +240,13 @@ def _run(tmp):
|
|
|
239
240
|
out_r.get("committed") is False and _fm(cg, "mem_rej0001") == {},
|
|
240
241
|
repr(out_r))
|
|
241
242
|
|
|
242
|
-
|
|
243
|
+
# 迁移(issue #43,2026-09-29):修前「未设 MDCG_POLICY_FILE」= 空规则 →
|
|
244
|
+
# text 恒 DEFER;修后未设会**回落到包内默认 data/policy.json**(本用例的
|
|
245
|
+
# 内容无 CCG 六要素 → 变 REJECT)。本用例的语义是 **DEFER 出口的副作用**
|
|
246
|
+
# (非 ACCEPT/REJECT 不落盘、不建边),与策略来源无关,故显式给一份**空
|
|
247
|
+
# 规则库**复现 DEFER——不改断言口径,只把「来源」钉成显式的。
|
|
248
|
+
os.environ["MDCG_POLICY_FILE"] = _policy(tmp, required=(), forbidden=(),
|
|
249
|
+
name="policy_empty.json")
|
|
243
250
|
out_d = pipe.execute(cg, {"content_kind": "text",
|
|
244
251
|
"content": "无规则可判 关联 %s" % TGT,
|
|
245
252
|
"layer": "knowledge", "node_id": "mem_def0001"})
|
package/md_cg/test_lock.py
CHANGED
|
@@ -19,7 +19,7 @@ def worker(m):
|
|
|
19
19
|
if not lk.acquired:
|
|
20
20
|
print("LOCK_TIMEOUT", file=sys.stderr)
|
|
21
21
|
try:
|
|
22
|
-
with open(TARGET) as f:
|
|
22
|
+
with open(TARGET, encoding="utf-8") as f:
|
|
23
23
|
v = int(f.read().strip() or 0)
|
|
24
24
|
except (OSError, ValueError):
|
|
25
25
|
v = 0
|
|
@@ -35,7 +35,7 @@ if __name__ == "__main__":
|
|
|
35
35
|
ps = [subprocess.Popen([sys.executable, os.path.abspath(__file__), "--worker", str(M)])
|
|
36
36
|
for _ in range(N)]
|
|
37
37
|
[p.wait() for p in ps]
|
|
38
|
-
with open(TARGET) as f:
|
|
38
|
+
with open(TARGET, encoding="utf-8") as f:
|
|
39
39
|
got = int(f.read().strip())
|
|
40
40
|
print(f"期望 {N*M},实际 {got} → {'互斥正常' if got == N*M else '★锁失效,丢了 %d 次' % (N*M-got)}")
|
|
41
41
|
os.remove(TARGET)
|