@furongjun1999/dsh-memory 0.4.7 → 0.4.8

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.
Files changed (172) hide show
  1. package/README.md +92 -37
  2. package/codebuddy/CODEBUDDY.md +195 -185
  3. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
  4. package/docs/README.md +111 -0
  5. package/docs/discipline/harnesses.yaml +215 -152
  6. package/docs/discipline/templates/full.md.tmpl +60 -58
  7. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
  8. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
  9. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
  10. package/docs/experiments/m4-role-probe/census.py +86 -0
  11. package/docs/experiments/m4-role-probe/probe.py +89 -0
  12. package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
  13. package/docs/experiments/mapped_confidence/post_check.py +75 -0
  14. package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
  15. package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
  16. package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
  17. package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +9 -9
  18. package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
  19. 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 +261 -0
  20. package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
  21. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.4.md +148 -0
  22. package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
  23. package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
  24. package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
  25. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
  26. 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 +4 -4
  27. package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
  28. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
  29. package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
  30. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
  31. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
  32. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -0
  33. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
  34. package/docs/{ → theory/}/347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
  35. package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
  36. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -0
  37. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
  38. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
  39. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
  40. 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 +34 -6
  41. package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
  42. package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
  43. package/dsh/README.md +1 -1
  44. package/lib/hooks.d.ts +5 -0
  45. package/lib/hooks.js +10 -1
  46. package/lib/lib/prompt_safety.d.ts +48 -0
  47. package/lib/lib/prompt_safety.js +62 -0
  48. package/md_cg/audit.py +76 -2
  49. package/md_cg/bench_membench.py +10 -5
  50. package/md_cg/bench_progressive.py +48 -9
  51. package/md_cg/branches.py +275 -0
  52. package/md_cg/ccgc.py +884 -0
  53. package/md_cg/conformance.py +662 -0
  54. package/md_cg/consistency.py +1 -1
  55. package/md_cg/corpus.py +1 -1
  56. package/md_cg/eval_common.py +6 -5
  57. package/md_cg/forgetting.py +11 -3
  58. package/md_cg/fsutil.py +63 -1
  59. package/md_cg/identity.py +2 -2
  60. package/md_cg/insight.py +2 -2
  61. package/md_cg/lifecycle.py +262 -0
  62. package/md_cg/mcp_server.py +547 -140
  63. package/md_cg/mdcg.py +206 -18
  64. package/md_cg/mdcos.py +512 -50
  65. package/md_cg/metacognition.py +2 -2
  66. package/md_cg/mreview/__init__.py +25 -0
  67. package/md_cg/mreview/__main__.py +107 -0
  68. package/md_cg/mreview/bundle.py +170 -0
  69. package/md_cg/mreview/candidates.py +253 -0
  70. package/md_cg/mreview/govern.py +674 -0
  71. package/md_cg/mreview/locate.py +905 -0
  72. package/md_cg/mreview/pipeline.py +701 -0
  73. package/md_cg/mreview/rules/duplication.json +21 -0
  74. package/md_cg/mreview/rules/field_coverage.json +54 -0
  75. package/md_cg/mreview/rules/source_license.json +21 -0
  76. package/md_cg/mreview/rules/template_flow.json +21 -0
  77. package/md_cg/mreview/ruleset.py +238 -0
  78. package/md_cg/nodefile.py +38 -0
  79. package/md_cg/predict.py +24 -6
  80. package/md_cg/protect.py +4 -4
  81. package/md_cg/refindex.py +41 -2
  82. package/md_cg/scrub.py +5 -1
  83. package/md_cg/self_state.py +1 -1
  84. package/md_cg/semantic/en_normalizer.py +85 -25
  85. package/md_cg/sustain.py +18 -0
  86. package/md_cg/tasks.py +447 -0
  87. package/md_cg/test_action_derive.py +203 -0
  88. package/md_cg/test_audit_rotate.py +270 -0
  89. package/md_cg/test_branches.py +249 -0
  90. package/md_cg/test_ccg_perturb.py +1 -1
  91. package/md_cg/test_ccgc.py +423 -0
  92. package/md_cg/test_conformance.py +343 -0
  93. package/md_cg/test_en_pipeline.py +24 -0
  94. package/md_cg/test_health_scale.py +173 -0
  95. package/md_cg/test_identity_attribution.py +6 -0
  96. package/md_cg/test_index_durability.py +224 -0
  97. package/md_cg/test_lifecycle.py +309 -0
  98. package/md_cg/test_mr_m1.py +679 -0
  99. package/md_cg/test_mr_m2.py +587 -0
  100. package/md_cg/test_mr_m3.py +703 -0
  101. package/md_cg/test_mr_m4.py +485 -0
  102. package/md_cg/test_p1.py +1 -1
  103. package/md_cg/test_p11_consistency.py +1 -1
  104. package/md_cg/test_p12_metacognition.py +2 -2
  105. package/md_cg/test_p16_self_state.py +1 -1
  106. package/md_cg/test_p26_refindex.py +1 -1
  107. package/md_cg/test_p27_docindex.py +22 -2
  108. package/md_cg/test_p28_refcheck.py +1 -1
  109. package/md_cg/test_p29_session_ingest_export.py +1 -1
  110. package/md_cg/test_p2_mcp.py +7 -1
  111. package/md_cg/test_p38_contextualize.py +1 -1
  112. package/md_cg/test_p39_vision_evidence.py +1 -1
  113. package/md_cg/test_p40_refine_worklist.py +1 -1
  114. package/md_cg/test_p41_evolve_patrol.py +1 -1
  115. package/md_cg/test_p42_provenance.py +1 -1
  116. package/md_cg/test_p43_pooling.py +41 -13
  117. package/md_cg/test_read_clip.py +137 -0
  118. package/md_cg/test_review_conformance.py +310 -0
  119. package/md_cg/test_tasks.py +409 -0
  120. package/md_cg/test_tool_face.py +189 -0
  121. package/md_cg/test_twophase.py +286 -0
  122. package/md_cg/test_writepipe.py +210 -0
  123. package/md_cg/theory.py +1 -1
  124. package/md_cg/tokens.py +95 -5
  125. package/md_cg/tool_face.py +250 -0
  126. package/md_cg/twophase.py +221 -0
  127. package/md_cg/units.py +546 -0
  128. package/md_cg/vision_evidence.py +1 -1
  129. package/md_cg/weights.py +1 -1
  130. package/md_cg/whitebox.py +1 -1
  131. package/md_cg/whitebox_kb/data/verify_cache.json +28 -0
  132. package/md_cg/whitebox_kb/data/verify_savings.jsonl +46 -0
  133. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  134. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  135. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
  136. package/md_cg/writelimit.py +19 -3
  137. package/md_cg/writepipe.py +372 -0
  138. package/package.json +1 -1
  139. package/src/hooks.ts +10 -1
  140. package/src/lib/prompt_safety.ts +62 -0
  141. package/zcode/AGENTS.md +195 -185
  142. package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
  143. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
  144. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
  145. /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
  146. /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
  147. /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
  148. /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
  149. /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
  150. /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
  151. /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
  152. /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
  153. /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
  154. /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
  155. /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
  156. /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
  157. /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
  158. /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
  159. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
  160. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
  161. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
  162. /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
  163. /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
  164. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
  165. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
  166. /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
  167. /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
  168. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
  169. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
  170. /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
  171. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
  172. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
@@ -0,0 +1,286 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_twophase · 写入两段式提交(Pi 可学习优点 ③)
3
+
4
+ 验收口径(交接文档 §3③):
5
+ - 写入在**闸门通过后先落 intent**(含内容指纹),落盘完成再落 outcome;
6
+ - 「kill -9 模拟半途写入后重启」→ 对账**正确补账**(内容确已落盘)或
7
+ **如实标记**(未落盘 / 指纹不符),不假装成功;
8
+ - 对账幂等、账本 append-only、不重放写入;
9
+ - 未落盘的分支(闸门拒绝 / 入队)不产生 intent——没打算写就不记账。
10
+
11
+ 运行:python -m md_cg.test_twophase
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import shutil
18
+ import tempfile
19
+ import traceback
20
+
21
+ from . import twophase, writepipe
22
+ from .mdcos import MdCGSecure
23
+ from .security import Principal
24
+
25
+ _ok = 0
26
+ _fail = []
27
+ _seq = [0]
28
+
29
+
30
+ def check(name, cond, detail=""):
31
+ global _ok
32
+ if cond:
33
+ _ok += 1
34
+ print("[ok] " + name)
35
+ else:
36
+ _fail.append(name)
37
+ print("[FAIL] %s · %s" % (name, str(detail)[:240]))
38
+
39
+
40
+ def _mk_cg(tmp, role="designer"):
41
+ _seq[0] += 1
42
+ root = os.path.join(tmp, "root_%02d_%s" % (_seq[0], role))
43
+ p = Principal(tenant="default", actor="t_" + role, role=role,
44
+ can_write=(role != "guest"), can_admin=(role == "designer"))
45
+ return MdCGSecure(root, principal=p)
46
+
47
+
48
+ def _policy(tmp, required=("PASSED",), forbidden=("FORBIDDEN_WORD",)):
49
+ path = os.path.join(tmp, "policy.json")
50
+ with open(path, "w", encoding="utf-8") as f:
51
+ json.dump({"forbidden": list(forbidden),
52
+ "required": list(required)}, f)
53
+ os.environ["MDCG_POLICY_FILE"] = path
54
+ return path
55
+
56
+
57
+ def _lines(cg):
58
+ p = twophase.log_path(cg)
59
+ if not os.path.isfile(p):
60
+ return []
61
+ with open(p, encoding="utf-8", errors="replace") as f:
62
+ return [x for x in f.read().splitlines() if x.strip()]
63
+
64
+
65
+ def _recs(cg):
66
+ return twophase.records(cg)
67
+
68
+
69
+ def _idx(cg, nid):
70
+ return (cg.index.get("nodes") or {}).get(nid) or {}
71
+
72
+
73
+ def _phases(cg, nid=None):
74
+ return [r.get("phase") for r in _recs(cg)
75
+ if nid is None or r.get("id") == nid]
76
+
77
+
78
+ def _run(tmp):
79
+ _policy(tmp)
80
+ pipe = writepipe.install_default_gates(writepipe.WritePipeline())
81
+
82
+ # ---------- ① 正常落盘:intent + outcome 成对 ----------
83
+ cg = _mk_cg(tmp)
84
+ out = pipe.execute(cg, {"content_kind": "text",
85
+ "content": "两段式正常落盘 PASSED",
86
+ "layer": "knowledge"})
87
+ nid = out.get("id")
88
+ check("①1 写入成功(committed)", out.get("committed") is True, out)
89
+ recs = _recs(cg)
90
+ ph = [r["phase"] for r in recs if r.get("id") == nid]
91
+ check("①2 账本两行成对(intent → outcome)", ph == ["intent", "outcome"], ph)
92
+ intent = [r for r in recs if r.get("id") == nid
93
+ and r["phase"] == "intent"][0]
94
+ oc = [r for r in recs if r.get("id") == nid and r["phase"] == "outcome"][0]
95
+ check("①3 intent 带内容指纹(对账的判据)",
96
+ bool(intent.get("content_hash")), intent)
97
+ check("①4 outcome 记 committed + 同 iid",
98
+ oc.get("status") == twophase.STATUS_COMMITTED
99
+ and oc.get("iid") == intent.get("iid"), oc)
100
+ check("①5 指纹走 nodefile 唯一真源(含写盘尾换行归一)",
101
+ intent.get("content_hash")
102
+ == twophase.nodefile.content_hash("两段式正常落盘 PASSED"),
103
+ (intent.get("content_hash"),
104
+ twophase.nodefile.content_hash("两段式正常落盘 PASSED")))
105
+ check("①6 正常写入后无未结清意图", cg.pending_writes() == [])
106
+
107
+ # ---------- ② intent 真的**先于**落盘(真实执行路径内观测) ----------
108
+ cg2 = _mk_cg(tmp)
109
+ _orig = writepipe._executor
110
+ seen = {}
111
+
112
+ def _probe(ctx):
113
+ seen["has_intent"] = any(
114
+ r.get("phase") == "intent" and r.get("id") == ctx["nid"]
115
+ for r in _recs(ctx["cg"]))
116
+ seen["has_outcome"] = any(
117
+ r.get("phase") == "outcome" and r.get("id") == ctx["nid"]
118
+ for r in _recs(ctx["cg"]))
119
+ seen["node_exists"] = bool(ctx["cg"].get(ctx["nid"]))
120
+ return _orig(ctx)
121
+
122
+ writepipe._executor = _probe
123
+ try:
124
+ pipe.execute(cg2, {"content_kind": "text",
125
+ "content": "先行持久化探针 PASSED",
126
+ "layer": "knowledge"})
127
+ finally:
128
+ writepipe._executor = _orig
129
+ check("②1 进入执行器时 intent 已落账(先行持久化成立)",
130
+ seen.get("has_intent") is True, seen)
131
+ check("②2 进入执行器时**尚无** outcome 且节点未落盘",
132
+ seen.get("has_outcome") is False
133
+ and seen.get("node_exists") is False, seen)
134
+
135
+ # ---------- ③ 半途崩溃:落盘已完成、outcome 未写 ----------
136
+ cg3 = _mk_cg(tmp)
137
+ nid3 = "mem_2pc_c3"
138
+ tok = twophase.begin(cg3, nid3, "崩溃在落盘之后 PASSED", layer="knowledge",
139
+ actor="test:crash")
140
+ cg3.add(nid3, "崩溃在落盘之后 PASSED", layer="knowledge")
141
+ check("③1 崩溃现场:有 intent 无 outcome",
142
+ _phases(cg3, nid3) == ["intent"]
143
+ and len(cg3.pending_writes()) == 1)
144
+ rep = cg3.reconcile_writes()
145
+ check("③2 对账补账(内容指纹吻合 → committed)",
146
+ rep.get("unpaired") == 1 and rep.get("committed") == 1
147
+ and rep.get("interrupted") == 0, rep)
148
+ check("③3 补出的 outcome 标 reconciled + 判据可审计",
149
+ any(r.get("phase") == "outcome" and r.get("reconciled")
150
+ and r.get("reason") == twophase.R_NODE_OK
151
+ for r in _recs(cg3)), _recs(cg3)[-1])
152
+ check("③4 对账后 pending 归零",
153
+ cg3.pending_writes() == [])
154
+
155
+ # ---------- ④ 半途崩溃:根本没落盘 ----------
156
+ cg4 = _mk_cg(tmp)
157
+ twophase.begin(cg4, "mem_2pc_c4", "崩溃在落盘之前 PASSED",
158
+ layer="knowledge")
159
+ rep4 = cg4.reconcile_writes()
160
+ check("④1 未落盘 → 如实标记 interrupted(不假装成功)",
161
+ rep4.get("unpaired") == 1 and rep4.get("interrupted") == 1
162
+ and rep4.get("committed") == 0, rep4)
163
+ check("④2 interrupted 判据 = node_missing",
164
+ rep4["details"][0]["reason"] == twophase.R_NODE_MISSING, rep4)
165
+ check("④3 未落盘节点确实不在库(标记与实际一致)",
166
+ cg4.get("mem_2pc_c4") is None)
167
+
168
+ # ---------- ⑤ 指纹不符:落了别的正文 ----------
169
+ cg5 = _mk_cg(tmp)
170
+ twophase.begin(cg5, "mem_2pc_c5", "意图写的是这一段 PASSED")
171
+ cg5.add("mem_2pc_c5", "实际落的是另一段 PASSED", layer="knowledge")
172
+ rep5 = cg5.reconcile_writes()
173
+ check("⑤1 指纹不符 → interrupted(content_mismatch)",
174
+ rep5.get("interrupted") == 1
175
+ and rep5["details"][0]["reason"] == twophase.R_MISMATCH, rep5)
176
+ check("⑤2 对账**不覆盖**既有正文(只标记不重放)",
177
+ "实际落的是另一段" in (cg5.get("mem_2pc_c5") or {}).get("content", ""))
178
+
179
+ # ---------- ⑥ dry-run / 幂等 / append-only ----------
180
+ cg6 = _mk_cg(tmp)
181
+ twophase.begin(cg6, "mem_2pc_c6", "dry-run 用例 PASSED")
182
+ before = _lines(cg6)
183
+ dry = cg6.reconcile_writes(apply=False)
184
+ check("⑥1 dry-run 报告未结清但不改账本",
185
+ dry.get("unpaired") == 1 and dry.get("applied") is False
186
+ and _lines(cg6) == before, dry)
187
+ cg6.reconcile_writes()
188
+ n_after_first = len(_lines(cg6))
189
+ rep6b = cg6.reconcile_writes()
190
+ check("⑥2 对账幂等(二次无未结清、不追加行)",
191
+ rep6b.get("unpaired") == 0
192
+ and len(_lines(cg6)) == n_after_first, rep6b)
193
+ check("⑥3 账本 append-only(原记录逐字保留)",
194
+ _lines(cg6)[:len(before)] == before)
195
+ snap = _lines(cg6)
196
+ cg6.pending_writes()
197
+ check("⑥4 pending 只读(不改账本)", _lines(cg6) == snap)
198
+
199
+ # ---------- ⑦ 执行器抛异常 → outcome=error,异常照抛 ----------
200
+ cg7 = _mk_cg(tmp, role="guest") # guest 无写权限 → 库层拒绝
201
+ pipe7 = writepipe.WritePipeline() # 不装闸:直接打执行器
202
+ raised = None
203
+ try:
204
+ pipe7.execute(cg7, {"content": "无权限写入 PASSED",
205
+ "layer": "knowledge"})
206
+ except Exception as exc: # AccessDenied 或其子类
207
+ raised = type(exc).__name__
208
+ check("⑦1 库层权限拒绝照抛(两段式不改错误语义)", raised is not None,
209
+ raised)
210
+ check("⑦2 失败也留痕:outcome=error 且无 committed",
211
+ any(r.get("phase") == "outcome"
212
+ and r.get("status") == twophase.STATUS_ERROR
213
+ for r in _recs(cg7)), _recs(cg7))
214
+ check("⑦3 失败不留未结清意图(已配对)", cg7.pending_writes() == [])
215
+
216
+ # ---------- ⑧ 未落盘的分支不记账 ----------
217
+ cg8 = _mk_cg(tmp)
218
+ out8 = pipe.execute(cg8, {"content_kind": "text",
219
+ "content": "这段含 FORBIDDEN_WORD 应被拒",
220
+ "layer": "knowledge"})
221
+ check("⑧1 闸门拒绝分支:committed=False",
222
+ out8.get("committed") is False, out8.get("moved_to"))
223
+ check("⑧2 未落盘 → **不产生** intent(没打算写就不记账)",
224
+ _phases(cg8) == [], _recs(cg8))
225
+ check("⑧3 对账在空账本上恒无未结清",
226
+ cg8.reconcile_writes().get("unpaired") == 0)
227
+
228
+ # ---------- ⑨ gated 替代执行路径也成对 ----------
229
+ cg9 = _mk_cg(tmp)
230
+ out9 = pipe.execute(cg9, {"content_kind": "text",
231
+ "content": "gated 主动遗忘路径 PASSED",
232
+ "layer": "contextual", "gated": True,
233
+ "importance": 0.5})
234
+ nid9 = out9.get("id")
235
+ ph9 = _phases(cg9, nid9)
236
+ check("⑨1 gated 路径账本成对(intent → outcome)",
237
+ ph9 == ["intent", "outcome"], ph9)
238
+ oc9 = [r for r in _recs(cg9)
239
+ if r.get("id") == nid9 and r["phase"] == "outcome"][0]
240
+ check("⑨2 结局与 verdict 一致(committed 当且仅当落盘)",
241
+ (oc9.get("status") == twophase.STATUS_COMMITTED)
242
+ == bool(out9.get("committed")), (oc9, out9.get("committed")))
243
+ check("⑨3 gated 结局无未结清(自己落盘的路径也结清)",
244
+ cg9.pending_writes() == [])
245
+
246
+ # ---------- ⑩ 边界 ----------
247
+ cg10 = _mk_cg(tmp)
248
+ check("⑩1 空账本:records/pending/reconcile 均不炸",
249
+ _recs(cg10) == [] and cg10.pending_writes() == []
250
+ and cg10.reconcile_writes().get("unpaired") == 0)
251
+ bad = os.path.join(cg10.root, "_write_2pc.jsonl")
252
+ with open(bad, "a", encoding="utf-8") as f:
253
+ f.write("{坏行不是 json\n")
254
+ check("⑩2 坏行跳过不炸(账本读侧容错)",
255
+ isinstance(_recs(cg10), list))
256
+ check("⑩3 指纹算法与 nodefile 同源(唯一真源,防漂移)",
257
+ twophase.nodefile.content_hash("abc")
258
+ == __import__("hashlib").sha256(b"abc").hexdigest()[:12])
259
+ check("⑩4 账本文件在 root 下(随记忆库一起备份/迁移)",
260
+ os.path.dirname(twophase.log_path(cg10)) == cg10.root)
261
+ check("⑩5 对账只增行不删行(append-only 结构保证)",
262
+ len(_lines(cg10)) >= 1 and all(l.strip() for l in _lines(cg10)))
263
+
264
+
265
+ def main():
266
+ tmp = tempfile.mkdtemp(prefix="twophase_")
267
+ old = os.environ.pop("MDCG_POLICY_FILE", None)
268
+ try:
269
+ _run(tmp)
270
+ except Exception:
271
+ traceback.print_exc()
272
+ _fail.append("未捕获异常")
273
+ finally:
274
+ shutil.rmtree(tmp, ignore_errors=True)
275
+ if old is not None:
276
+ os.environ["MDCG_POLICY_FILE"] = old
277
+ else:
278
+ os.environ.pop("MDCG_POLICY_FILE", None)
279
+ print("\ntest_twophase: %d 通过 / %d 失败" % (_ok, len(_fail)))
280
+ if _fail:
281
+ print("失败项:" + "、".join(_fail))
282
+ raise SystemExit(1 if _fail else 0)
283
+
284
+
285
+ if __name__ == "__main__":
286
+ main()
@@ -0,0 +1,210 @@
1
+ # -*- coding: utf-8 -*-
2
+ """写入路径拦截器链(writepipe)验收:Pi 钩子化移植(交接文档 §3⑥)。
3
+
4
+ 验收口径:
5
+ - 默认链行为与重构前 _cg_dispatch write 分支逐字节一致
6
+ (ACCEPT 直写 / REJECT 负记忆 / DEFER 入队 + 幂等去重联动 / gated 闸 /
7
+ 冲突 cvd 透传);
8
+ - 新增/移除一个拦截器不改核心文件(register/unregister 即插即拔);
9
+ - 角色/层权限校验结构上不可被拦截器绕过(反面清单:信任必须结构强制);
10
+ - REWRITE(拦截器改写 ctx["a"]["content"])/ 链序 / 短路 / after 观测者。
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import shutil
16
+ import sys
17
+ import tempfile
18
+ import traceback
19
+
20
+ from . import writepipe
21
+ from .mdcos import MdCGSecure
22
+ from .security import AccessDenied, Principal
23
+
24
+ _ok = 0
25
+ _bad = []
26
+ _seq = [0]
27
+
28
+
29
+ def _check(name, cond, detail=""):
30
+ global _ok
31
+ if cond:
32
+ _ok += 1
33
+ print(" ok %s" % name)
34
+ else:
35
+ _bad.append("%s %s" % (name, detail))
36
+ print(" FAIL %s %s" % (name, detail))
37
+
38
+
39
+ def _mk_cg(tmp, role="designer"):
40
+ _seq[0] += 1
41
+ root = os.path.join(tmp, "root_%02d_%s" % (_seq[0], role))
42
+ p = Principal(tenant="default", actor="t_" + role, role=role,
43
+ can_write=(role != "guest"),
44
+ can_admin=(role == "designer"))
45
+ return MdCGSecure(root, principal=p)
46
+
47
+
48
+ def _policy(tmp, required=("PASSED",), forbidden=("FORBIDDEN_WORD",)):
49
+ path = os.path.join(tmp, "policy.json")
50
+ with open(path, "w", encoding="utf-8") as f:
51
+ json.dump({"forbidden": list(forbidden),
52
+ "required": list(required)}, f)
53
+ os.environ["MDCG_POLICY_FILE"] = path
54
+ return path
55
+
56
+
57
+ def main():
58
+ tmp = tempfile.mkdtemp(prefix="writepipe_")
59
+ old_policy = os.environ.pop("MDCG_POLICY_FILE", None)
60
+ try:
61
+ _run(tmp)
62
+ except Exception:
63
+ traceback.print_exc()
64
+ _bad.append("未捕获异常")
65
+ finally:
66
+ shutil.rmtree(tmp, ignore_errors=True)
67
+ if old_policy is not None:
68
+ os.environ["MDCG_POLICY_FILE"] = old_policy
69
+ else:
70
+ os.environ.pop("MDCG_POLICY_FILE", None)
71
+ print("\nwritepipe 验收:%d 通过%s" % (
72
+ _ok, (",%d 失败:%s" % (len(_bad), "; ".join(_bad))) if _bad else ""))
73
+ return 1 if _bad else 0
74
+
75
+
76
+ def _run(tmp):
77
+ policy = _policy(tmp)
78
+
79
+ # ---------- 1 默认链:与重构前 write 分支行为一致 ----------
80
+ pipe = writepipe.install_default_gates(writepipe.WritePipeline())
81
+ cg = _mk_cg(tmp)
82
+
83
+ out = pipe.execute(cg, {"content_kind": "text",
84
+ "content": "要素齐全 PASSED",
85
+ "layer": "knowledge"})
86
+ _check("默认链ACCEPT直写", out.get("committed") is True and out.get("ok") is True,
87
+ repr(out))
88
+ _check("冲突闸cvd透传", isinstance(out.get("consistency"), dict), repr(out))
89
+
90
+ out = pipe.execute(cg, {"content_kind": "text",
91
+ "content": "含 FORBIDDEN_WORD 的违禁内容",
92
+ "layer": "knowledge"})
93
+ _check("REJECT负记忆", out.get("moved_to") == "rejected"
94
+ and out.get("committed") is False, repr(out))
95
+
96
+ # DEFER(无规则文件 → 无法判定 → 入审核队列)+ 幂等去重联动
97
+ os.environ.pop("MDCG_POLICY_FILE", None)
98
+ a = {"content_kind": "text", "content": "无规则可判的内容",
99
+ "layer": "knowledge"}
100
+ out1 = pipe.execute(cg, dict(a))
101
+ _check("DEFER入队", out1.get("moved_to") == "review_queue"
102
+ and bool(out1.get("pid")), repr(out1))
103
+ out2 = pipe.execute(cg, dict(a))
104
+ _check("幂等去重联动", out2.get("dedup") is True
105
+ and out2.get("dup_of") == out1.get("pid"), repr(out2))
106
+ os.environ["MDCG_POLICY_FILE"] = policy
107
+
108
+ out = pipe.execute(cg, {"content_kind": "text",
109
+ "content": "PASSED 情境记忆", "gated": True,
110
+ "layer": "contextual"})
111
+ _check("gated闸代执行", "gate" in out, repr(out))
112
+
113
+ # ---------- 2 注册/移除拦截器不改核心文件 ----------
114
+ singleton = writepipe.default_pipeline()
115
+
116
+ def _block_skipme(ctx):
117
+ if "SKIPME" in (ctx["a"].get("content") or ""):
118
+ return {"ok": False, "committed": False,
119
+ "moved_to": "hook_blocked", "by": "test_block"}
120
+ return None
121
+
122
+ singleton.register_before("test_block", _block_skipme)
123
+ out = singleton.execute(cg, {"content_kind": "text",
124
+ "content": "PASSED SKIPME",
125
+ "layer": "knowledge"})
126
+ _check("注册即拦", out.get("moved_to") == "hook_blocked", repr(out))
127
+ singleton.unregister_before("test_block")
128
+ out = singleton.execute(cg, {"content_kind": "text",
129
+ "content": "PASSED SKIPME",
130
+ "layer": "knowledge"})
131
+ _check("移除即恢复(核心文件零改动)", out.get("committed") is True, repr(out))
132
+ _check("单例默认链", singleton.names()["before"]
133
+ == ["audit", "consistency", "gated"],
134
+ repr(singleton.names()))
135
+
136
+ # ---------- 3 链序 / 短路 / REWRITE ----------
137
+ p3 = writepipe.WritePipeline()
138
+ order = []
139
+ p3.register_before("g1", lambda ctx: order.append("g1") or None)
140
+ p3.register_before("g2", lambda ctx: order.append("g2") or None)
141
+ p3.register_before("g3", lambda ctx: order.append("g3")
142
+ or {"ok": False, "moved_to": "halt"})
143
+ cg3 = _mk_cg(tmp)
144
+ out = p3.execute(cg3, {"content_kind": "text", "content": "PASSED",
145
+ "layer": "knowledge"})
146
+ _check("链序按注册序", order == ["g1", "g2", "g3"], repr(order))
147
+ _check("终态短路不落盘", out.get("moved_to") == "halt"
148
+ and "id" not in out and "committed" not in out, repr(out))
149
+
150
+ p4 = writepipe.WritePipeline()
151
+ p4.register_before("rw", lambda ctx:
152
+ ctx["a"].__setitem__("content",
153
+ ctx["a"].get("content") + " REWRITTEN")
154
+ or None)
155
+ cg4 = _mk_cg(tmp)
156
+ out = p4.execute(cg4, {"content_kind": "text", "content": "PASSED",
157
+ "layer": "knowledge"})
158
+ _check("REWRITE改写生效", out.get("committed") is True
159
+ and bool(cg4.search("REWRITTEN")), repr(out))
160
+
161
+ # ---------- 4 权限结构上不可被拦截器绕过 ----------
162
+ cg_guest = _mk_cg(tmp, role="guest")
163
+ p5 = writepipe.WritePipeline()
164
+ p5.register_before("always_pass", lambda ctx: None)
165
+ try:
166
+ p5.execute(cg_guest, {"content_kind": "text", "content": "PASSED",
167
+ "consistency": False, "layer": "knowledge"})
168
+ _check("权限不可绕", False, "guest 写入未被拒")
169
+ except AccessDenied:
170
+ _check("权限不可绕", True)
171
+ except Exception as e: # noqa: BLE001
172
+ _check("权限不可绕", False,
173
+ "异常类型=%s:%s" % (type(e).__name__, e))
174
+
175
+ # ---------- 5 after 观测者 ----------
176
+ p6 = writepipe.WritePipeline()
177
+ seen = []
178
+ p6.register_after("spy", lambda ctx, out: seen.append(out.get("committed")))
179
+ cg6 = _mk_cg(tmp)
180
+ p6.execute(cg6, {"content_kind": "text", "content": "PASSED",
181
+ "layer": "knowledge"})
182
+ _check("after观测成功", seen == [True], repr(seen))
183
+
184
+ def _boom(ctx, out):
185
+ raise RuntimeError("after钩子故障")
186
+
187
+ p6.register_after("boom", _boom)
188
+ try:
189
+ p6.execute(cg6, {"content_kind": "text", "content": "PASSED 二次",
190
+ "layer": "knowledge"})
191
+ _check("after故障不吞", False, "异常被静默")
192
+ except RuntimeError:
193
+ _check("after故障不吞", True)
194
+
195
+ # ---------- 6 注册表卫生 ----------
196
+ p7 = writepipe.WritePipeline()
197
+ p7.register_before("x", lambda ctx: None)
198
+ p7.register_before("x", lambda ctx: None) # 同名幂等替换
199
+ _check("同名幂等注册", p7.names()["before"] == ["x"], repr(p7.names()))
200
+ try:
201
+ p7.register_before("y", 123)
202
+ _check("类型守卫", False, "非可调用未拒")
203
+ except TypeError:
204
+ _check("类型守卫", True)
205
+ _check("注销返回布尔", p7.unregister_before("x") is True
206
+ and p7.unregister_before("x") is False)
207
+
208
+
209
+ if __name__ == "__main__":
210
+ sys.exit(main())
package/md_cg/theory.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """md_cg · 版本层(蜂群互联 · 层0)
3
3
 
4
- 设计依据:`docs/蜂群互联_v0.1.md` §2.3 / §3.1 / §10。
4
+ 设计依据:`docs/swarm/蜂群互联_v0.1.md` §2.3 / §3.1 / §10。
5
5
 
6
6
  版本更新权 = 蜂群**唯一核心权限**(严格唯一,仅 designer)。本模块只做
7
7
  「声明 + 校验」两件事,不做跨节点证据交换(§9.1 属存在约束,留待 v0.3):
package/md_cg/tokens.py CHANGED
@@ -53,7 +53,9 @@ ALL_LAYERS = ("anchor", "structural", "knowledge", "contextual", "self",
53
53
  "rejected", "unresolved", "goals")
54
54
 
55
55
  # 认知图 op 全集(与 mcp_server._cg_call 对齐,供 ops_allow 收窄)
56
- ALL_OPS = ("info", "route", "read", "write", "goal", "recent", "verify",
56
+ # help 按需披露入口:工具面投影(见 md_cg/tool_face.py)后,被外置的完整
57
+ # op/参数语义在此从真源取回;只读元信息,不经角色闸(不属于写/裁决面)
58
+ ALL_OPS = ("help", "info", "route", "read", "write", "goal", "task", "recent", "verify",
57
59
  "review", "forget", "protect", "identity", "consistency",
58
60
  "metacognition", "self_state", "evolution", "sustain", "scrub",
59
61
  "predict", "causal", "whitebox", "index_code", "index_doc", "ref",
@@ -71,7 +73,13 @@ ALL_OPS = ("info", "route", "read", "write", "goal", "recent", "verify",
71
73
  # 情景重构(reconstruct)+ 盲区学习(learn)+
72
74
  # 结构洞察(outlook);写入 action 按 can_write 收窄,
73
75
  # learn/apply 与批量落库走 require_admin
74
- "session", "ingest", "export", "maintain", "consolidate", "insight")
76
+ "session", "ingest", "export", "maintain", "consolidate", "insight",
77
+ # P3 新增(记忆可靠性闸,见 docs 讨论):
78
+ # ccg CCG 六要素编译器(compile/review/attest/link/recalibrate/units)
79
+ # —— 对话记录→六要素候选→**编外复核**→落库;裁定 A:编译者不得自证
80
+ # (E041 机械拒绝 verifier == compiled_by)。复核通道优先蜂巢
81
+ # reflect/verify 单元,不可用则提示配置或降级 harness 端子代理。
82
+ "ccg")
75
83
 
76
84
 
77
85
  class TokenError(Exception):
@@ -102,8 +110,12 @@ ROLE_SPECS = OrderedDict([
102
110
  # session=记会话要点;ingest=摄取外部文件流(记录单元本职)
103
111
  # maintain=写入前馈 prefeed(apply 类批量改写仍被 require_admin 拦截)
104
112
  # insight=记录洞见事件(record);verify/learn 在分发层按单位职责收窄
105
- "ops_allow": ["info", "route", "read", "write", "goal", "recent",
106
- "session", "ingest", "maintain", "insight"],
113
+ # ccg=CCG 六要素编译器(记录单元本职:保存观测/过程/结果与误差);
114
+ # 其准入不靠 admin 闸而靠签章机械闸(E040/E041/E042)——编译者不得自证
115
+ # task=结构层任务台账(工程做到哪一步/结果是什么)——记录单元本职:
116
+ # 保存过程与结果;「不得自证」由 done 时的结果必填闸承接
117
+ "ops_allow": ["info", "route", "read", "write", "goal", "task", "recent",
118
+ "session", "ingest", "maintain", "insight", "ccg"],
107
119
  "delegable": False,
108
120
  "forbidden": ["self/anchor 层", "private/secret 密级", "裁决与删除"],
109
121
  }),
@@ -177,6 +189,32 @@ POSITION_ROLES = ("record", "reflect", "verify", "output", "sustain")
177
189
 
178
190
  DELEGABLE_ROLES = tuple(r for r, s in ROLE_SPECS.items() if s["delegable"])
179
191
 
192
+ # --------------------------------------------------------------------------
193
+ # 蜂巢编排器(hive/orch.py)派生收窄面 · 单一真源
194
+ # --------------------------------------------------------------------------
195
+ # 设计依据(取证 2026-09-16,源码级:本文件 derive() + md_cg/mdcos.py 的
196
+ # require_admin 闸门):
197
+ # ① 编排器要「能裁决子代理冲突」→ 必须过 `MdCGSecure.review_decide` 的库层
198
+ # `require_admin`。而 `derive()` 的 `can_admin = spec.can_admin and
199
+ # parent.can_admin`(**不可收窄**)→ 唯一可行 role 是 designer。
200
+ # (`narrowed_principal()` 的 can_admin 恒 False,裁决会被库层拒 —— 这是被
201
+ # 代码证据排除的路径,不是偏好取舍。)
202
+ # ② 因此「不给 delete / 不给 anchor / 不给 delegate」**不能依赖 admin 闸**
203
+ # (can_admin=True 时该闸不拦清单内的 op),而由三重结构保证:
204
+ # · ops_allow 白名单(ALL_OPS 的**子集**)—— 不在清单的 op 被
205
+ # `_cg_dispatch` 前置 `require_op` 直接拒:forget=删除、identity=动地基、
206
+ # protect=固化、maintain/consolidate 的 apply 类批量改写,全在清单外;
207
+ # · layers_allow 白名单(排除 CORE_LAYERS=anchor/self)—— 写不进地基;
208
+ # · `derive()` 硬编码 `delegable=False` —— 派生令牌结构上不可再派生。
209
+ # 白名单方向 fail-closed:ALL_OPS 将来新增 op,默认不在清单内 = 不给。
210
+ # ③ 残余面(诚实标注,未收窄):can_admin=True 使清单内 op 的 admin 分支仍可
211
+ # 通过 —— `recent` 的 clear、`consistency` 的 auto_flywheel 写、`review` 的
212
+ # 裁决(功能所求)。改本常量即改编排器权限:签发(CLI `orch`)与
213
+ # hive/orch.py 同引此处,防两处硬编码漂移。
214
+ ORCH_ROLE = "designer"
215
+ ORCH_OPS_ALLOW = ("route", "read", "write", "review", "recent", "consistency")
216
+ ORCH_LAYERS_ALLOW = tuple(l for l in ALL_LAYERS if l not in CORE_LAYERS)
217
+
180
218
 
181
219
  def normalize_role(role: str) -> str:
182
220
  r = (role or "").strip().lower()
@@ -464,6 +502,22 @@ def _csv_list(v):
464
502
  return [x.strip() for x in str(v).split(",") if x.strip()] or None
465
503
 
466
504
 
505
+ def _write_secret(path: str, text: str) -> None:
506
+ """把令牌明文写入文件(0600,原子替换)——供 HIVE_ORCH_TOKEN_FILE 读取。"""
507
+ p = os.path.abspath(path)
508
+ d = os.path.dirname(p)
509
+ if d:
510
+ os.makedirs(d, exist_ok=True)
511
+ tmp = p + ".tmp"
512
+ with open(tmp, "w", encoding="utf-8") as f:
513
+ f.write(text)
514
+ os.replace(tmp, p)
515
+ try:
516
+ os.chmod(p, 0o600)
517
+ except OSError:
518
+ pass
519
+
520
+
467
521
  def main(argv=None):
468
522
  ap = argparse.ArgumentParser(
469
523
  prog="python -m md_cg.tokens",
@@ -491,6 +545,23 @@ def main(argv=None):
491
545
  p_d.add_argument("--actor", default=None)
492
546
  p_d.add_argument("--ttl", type=float, default=None)
493
547
  p_d.add_argument("--label", default="")
548
+ p_d.add_argument("--ops-allow", dest="ops_allow", default=None,
549
+ help="收窄 op 白名单(逗号分隔;越界项被忽略,只能小于角色默认)")
550
+ p_d.add_argument("--layers-allow", dest="layers_allow", default=None,
551
+ help="收窄可写层白名单(逗号分隔;越界项被忽略)")
552
+ p_d.add_argument("--clearance", default=None,
553
+ help="收窄密级(默认取角色上限,且不超过父令牌)")
554
+
555
+ p_o = sub.add_parser(
556
+ "orch",
557
+ help="签发蜂巢编排器令牌(收窄面取自本模块 ORCH_* 真源,一步到位)")
558
+ p_o.add_argument("--token", default=None, help="父令牌明文(须为 designer 且可派生)")
559
+ p_o.add_argument("--token-file-in", dest="token_file_in", default=None,
560
+ help="从文件读父令牌")
561
+ p_o.add_argument("--ttl", type=float, default=None, help="有效期(秒)")
562
+ p_o.add_argument("--label", default="hive-orchestrator")
563
+ p_o.add_argument("--out", default=None,
564
+ help="把令牌明文写入该文件(0600;供 HIVE_ORCH_TOKEN_FILE 读取)")
494
565
 
495
566
  p_v = sub.add_parser("verify", help="校验令牌并打印身份")
496
567
  p_v.add_argument("--token", default=None)
@@ -516,7 +587,26 @@ def main(argv=None):
516
587
  with open(a.token_file_in, encoding="utf-8") as f:
517
588
  tok = f.read().strip()
518
589
  _print(derive(tok, a.role, actor=a.actor, ttl=a.ttl,
519
- label=a.label, path=a.token_file))
590
+ label=a.label, path=a.token_file,
591
+ clearance=a.clearance,
592
+ layers_allow=_csv_list(a.layers_allow),
593
+ ops_allow=_csv_list(a.ops_allow)))
594
+ elif a.cmd == "orch":
595
+ tok = a.token
596
+ if a.token_file_in:
597
+ with open(a.token_file_in, encoding="utf-8") as f:
598
+ tok = f.read().strip()
599
+ r = derive(tok, ORCH_ROLE, actor="hive-orchestrator", ttl=a.ttl,
600
+ label=a.label, path=a.token_file,
601
+ layers_allow=list(ORCH_LAYERS_ALLOW),
602
+ ops_allow=list(ORCH_OPS_ALLOW))
603
+ if a.out:
604
+ _write_secret(a.out, r["token"])
605
+ r = {k: v for k, v in r.items() if k != "token"}
606
+ r["token_written_to"] = os.path.abspath(a.out)
607
+ r["usage"] = ("把令牌明文放入 hive serve 的环境变量 HIVE_ORCH_TOKEN"
608
+ "(或写文件后设 HIVE_ORCH_TOKEN_FILE)再重启 serve")
609
+ _print(r)
520
610
  elif a.cmd == "verify":
521
611
  tok = a.token
522
612
  if a.token_file_in: