@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.
Files changed (156) hide show
  1. package/README.md +572 -465
  2. package/docs/README.md +1 -0
  3. 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
  4. 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
  5. package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
  6. 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
  7. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  8. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  37. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
  38. 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
  39. 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
  40. 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
  41. package/dsh/cordis-patch-profile-web.example.yml +35 -0
  42. package/lib/bridge.d.ts +9 -0
  43. package/lib/bridge.js +35 -0
  44. package/lib/cli.d.ts +3 -0
  45. package/lib/cli.js +66 -0
  46. package/lib/index.js +7 -1
  47. package/lib/init.d.ts +88 -0
  48. package/lib/init.js +287 -0
  49. package/lib/lib/roleplay_web.js +530 -443
  50. package/lib/lib/token_store.d.ts +7 -1
  51. package/lib/lib/token_store.js +12 -3
  52. package/md_cg/audit.py +12 -1
  53. package/md_cg/backfill.py +16 -15
  54. package/md_cg/backfill_bucket_zh.py +35 -0
  55. package/md_cg/bench_e2e_judge.py +532 -0
  56. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  57. package/md_cg/bench_e2e_qa.py +256 -0
  58. package/md_cg/branches.py +18 -2
  59. package/md_cg/ccgc.py +3 -2
  60. package/md_cg/chain.py +19 -4
  61. package/md_cg/consolidate.py +7 -6
  62. package/md_cg/crosscheck.py +4 -3
  63. package/md_cg/crypto.py +439 -437
  64. package/md_cg/datapath.py +395 -335
  65. package/md_cg/evidence.py +8 -3
  66. package/md_cg/export.py +3 -1
  67. package/md_cg/forgetting.py +2 -2
  68. package/md_cg/fsutil.py +48 -0
  69. package/md_cg/hotcache.py +255 -238
  70. package/md_cg/interop.py +161 -22
  71. package/md_cg/judgment_manifest.py +177 -0
  72. package/md_cg/links.py +655 -622
  73. package/md_cg/mcp_server.py +282 -31
  74. package/md_cg/mdcg.py +702 -126
  75. package/md_cg/mdcos.py +454 -68
  76. package/md_cg/mreview/govern.py +5 -4
  77. package/md_cg/postings.py +4 -2
  78. package/md_cg/readcache.py +76 -18
  79. package/md_cg/reconcile.py +228 -0
  80. package/md_cg/review_cli.py +215 -0
  81. package/md_cg/routing.py +28 -0
  82. package/md_cg/run_tests.py +211 -0
  83. package/md_cg/scrub.py +862 -852
  84. package/md_cg/security.py +385 -275
  85. package/md_cg/selfreport.py +3 -2
  86. package/md_cg/signer.py +565 -562
  87. package/md_cg/sources.py +3 -2
  88. package/md_cg/stg.py +6 -0
  89. package/md_cg/sustain.py +1168 -1138
  90. package/md_cg/test_access_hints.py +147 -0
  91. package/md_cg/test_branch_discard_tombstone.py +136 -0
  92. package/md_cg/test_branches.py +259 -249
  93. package/md_cg/test_chain_read_isolate.py +168 -0
  94. package/md_cg/test_datapath_device_name.py +203 -0
  95. package/md_cg/test_emit_negtail_cache.py +156 -0
  96. package/md_cg/test_en_pipeline.py +186 -166
  97. package/md_cg/test_govern_directread.py +421 -0
  98. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  99. package/md_cg/test_identity_attribution.py +228 -147
  100. package/md_cg/test_index_crossprocess_reload.py +301 -0
  101. package/md_cg/test_index_durability.py +238 -224
  102. package/md_cg/test_interop.py +4 -2
  103. package/md_cg/test_interop_judgment.py +228 -0
  104. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  105. package/md_cg/test_links_concurrent_write.py +188 -0
  106. package/md_cg/test_merge_upsert.py +168 -0
  107. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  108. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  109. package/md_cg/test_n131_merge_gate.py +205 -0
  110. package/md_cg/test_none_id_write_guard.py +165 -0
  111. package/md_cg/test_p1x_ref_root.py +160 -0
  112. package/md_cg/test_p27_docindex.py +774 -765
  113. package/md_cg/test_p2_mcp.py +3 -0
  114. package/md_cg/test_p32_backfill.py +304 -298
  115. package/md_cg/test_p39_verify_flow.py +90 -50
  116. package/md_cg/test_p47_session_view.py +60 -25
  117. package/md_cg/test_propose_tail_index.py +157 -0
  118. package/md_cg/test_read_scope_b27.py +277 -0
  119. package/md_cg/test_readcache_default_on.py +168 -0
  120. package/md_cg/test_readcache_precise_inval.py +270 -0
  121. package/md_cg/test_readcache_prodpath.py +55 -7
  122. package/md_cg/test_reconcile_v0.py +294 -0
  123. package/md_cg/test_retr_s1.py +6 -2
  124. package/md_cg/test_retr_s1b.py +67 -0
  125. package/md_cg/test_retr_s7.py +8 -0
  126. package/md_cg/test_retr_s9_entity_ctx.py +181 -175
  127. package/md_cg/test_retr_score_once.py +208 -0
  128. package/md_cg/test_review_cli_attribution.py +177 -0
  129. package/md_cg/test_review_cli_visibility.py +235 -0
  130. package/md_cg/test_review_onepass.py +170 -0
  131. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  132. package/md_cg/test_security_audit.py +155 -0
  133. package/md_cg/test_security_audit_b26.py +161 -0
  134. package/md_cg/test_security_audit_v21.py +250 -0
  135. package/md_cg/test_semantic_canonical.py +255 -241
  136. package/md_cg/test_session_isolation.py +168 -0
  137. package/md_cg/test_snapshot_autoclose.py +187 -0
  138. package/md_cg/test_tail_watermark_race.py +208 -0
  139. package/md_cg/test_tenant_env_override_warn.py +139 -0
  140. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  141. package/md_cg/test_v14_fixes.py +415 -397
  142. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  143. package/md_cg/theory.py +276 -273
  144. package/md_cg/tokens.py +734 -677
  145. package/md_cg/units.py +3 -2
  146. package/md_cg/vision_evidence.py +4 -3
  147. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  148. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  149. package/md_cg/writepipe.py +554 -550
  150. package/package.json +11 -2
  151. package/src/bridge.ts +434 -401
  152. package/src/cli.ts +65 -0
  153. package/src/index.ts +526 -518
  154. package/src/init.ts +347 -0
  155. package/src/lib/roleplay_web.ts +1019 -932
  156. package/src/lib/token_store.ts +202 -192
package/md_cg/signer.py CHANGED
@@ -1,563 +1,566 @@
1
- # -*- coding: utf-8 -*-
2
- """签名接口层(蜂群互联 D-4)。
3
-
4
- 设计裁决 D-4(2026-09-10,设计者):
5
-
6
- · 协议内核**不实现密码学**,只定义 `sign` / `verify` 契约;
7
- · 具体签名链(企业 CA / KMS / 签章服务)在**工程项目**中实现;
8
- · **搭载灵枢的智能体自行决定子系统如何使用签名链**(策略落盘 `_signers.json`)。
9
-
10
- 因此本模块只做三件事:
11
- ① 定义可插拔的 `Signer` 契约 + 注册表;
12
- ② 提供两个最简内置实现(`null` / `hmac-local`);
13
- ③ 读取并执行**子系统签名策略**(何时签、是否要求对端签、验签失败如何处置)。
14
-
15
- **内核不保存私钥**:私钥归工程侧,这里只持有 `sign` / `verify` 的可调用引用。
16
- 工程侧注入方式:
17
-
18
- # 1) 代码注入
19
- from md_cg.signer import register_signer
20
- register_signer("corp_pki", lambda: MyCorpSigner(...))
21
-
22
- # 2) 启动自动加载(MCP 子进程推荐)
23
- MDCG_SIGNER_MODULE=mycorp.signing # 模块内提供 register()
24
-
25
- 零第三方依赖。
26
- """
27
- from __future__ import annotations
28
-
29
- import argparse
30
- import base64
31
- import hashlib
32
- import hmac
33
- import importlib
34
- import json
35
- import os
36
- import secrets
37
- import sys
38
- import time
39
-
40
- SIGNER_ENV = "MDCG_SIGNER"
41
- SIGNER_MODULE_ENV = "MDCG_SIGNER_MODULE"
42
- SIGNERS_FILE_ENV = "MDCG_SIGNERS_FILE"
43
- KEY_FILE_ENV = "MDCG_SIGNER_KEY_FILE"
44
-
45
- DEFAULT_DIR = os.path.join(os.path.expanduser("~"), ".mdcg")
46
- DEFAULT_SIGNERS_FILE = os.path.join(DEFAULT_DIR, "_signers.json")
47
- DEFAULT_KEY_FILE = os.path.join(DEFAULT_DIR, "_signer.key")
48
-
49
- SCHEMA = 1
50
- DEFAULT_SIGNER = "hmac-local"
51
- PREFIX = "hmac1"
52
-
53
- # 需要签名的动作(子系统策略里引用)
54
- SIGN_ACTIONS = ("handshake", "evidence", "withdrawal")
55
- # 验签失败的处置档位(宪章第七条降级为默认)
56
- ON_VERIFY_FAIL = ("degrade", "isolate", "reject")
57
-
58
-
59
- class SignerError(Exception):
60
- """签名接口层错误。"""
61
-
62
-
63
- # --------------------------------------------------------------------------
64
- # 契约与内置实现
65
- # --------------------------------------------------------------------------
66
-
67
- # 生效条件:实现方提供可调用的 sign(payload, ctx) 与 verify(payload, signature, ctx) 即满足鸭子类型契约;name 缺省为 'abstract',基类实现直接抛 NotImplementedError。
68
- class Signer:
69
- """签名器契约。工程侧实现只需满足这两个方法(鸭子类型即可)。"""
70
-
71
- name = "abstract"
72
-
73
- # 生效条件:基类实现,任意 payload 下直接 raise NotImplementedError,由子类覆写。
74
- def sign(self, payload: bytes, ctx: dict = None) -> str:
75
- raise NotImplementedError
76
-
77
- # 生效条件:基类实现,任意 payload 与 signature 下直接 raise NotImplementedError,由子类覆写。
78
- def verify(self, payload: bytes, signature: str, ctx: dict = None) -> bool:
79
- raise NotImplementedError
80
-
81
- # 生效条件:无前置;返回 {"name": self.name, "kind": "abstract"},不含密钥材料,供注册表自描述用。
82
- def describe(self) -> dict:
83
- return {"name": self.name, "kind": "abstract"}
84
-
85
- # 生效条件:无 required 形参,调用即返回仅含 {'name': self.name} 的 dict,不含密钥材料。
86
- def public(self) -> dict:
87
- """可对外公开的验证信息(**不得含密钥**)。"""
88
- return {"name": self.name}
89
-
90
-
91
- # 生效条件:sign 对任意 payload 恒返回 '',verify 仅当 signature 为 '' 或 None 时返回 True(name 为 'null')。
92
- class NullSigner(Signer):
93
- """不签名(观察期 / 纯本地)。验签时把「空签名」视为通过。"""
94
-
95
- name = "null"
96
-
97
- # 生效条件:任意 payload 下均返回空串 "",不做任何签名计算。
98
- def sign(self, payload: bytes, ctx: dict = None) -> str:
99
- return ""
100
-
101
- # 生效条件:signature 恰为 "" 或 None 时返回 True,其余取值(含仅含空白的字符串)返回 False,与 payload 无关。
102
- def verify(self, payload: bytes, signature: str, ctx: dict = None) -> bool:
103
- return signature in ("", None)
104
-
105
- # 生效条件:无前置;返回 {"name", "kind": "none", "note"},其中 note 明示「不签名;仅用于观察期或纯本地场景」,不含密钥材料。
106
- def describe(self) -> dict:
107
- return {"name": self.name, "kind": "none",
108
- "note": "不签名;仅用于观察期或纯本地场景"}
109
-
110
-
111
- # 生效条件:__init__ 无必需形参;key_file 为假值(None/空串)时回落 key_file_path(),key 为假值(None/空串)时回落 load_key(self.key_file)。
112
- class HmacLocalSigner(Signer):
113
- """本地 HMAC-SHA256 实现——与 `_tokens.json` 同一信任根级别的最简方案。
114
-
115
- 它**不是**企业级签名链(无不可否认性、无私钥托管),
116
- 仅用于单机自测与「有签名位置但暂无 CA」的过渡期。
117
- """
118
-
119
- name = "hmac-local"
120
-
121
- # 生效条件:key_file 为假值(None/空串)时 self.key_file 回落 key_file_path();key 为假值(None/空字节串)时 self.key 回落 load_key(self.key_file)。
122
- def __init__(self, key: bytes = None, key_file: str = None):
123
- self.key_file = key_file or key_file_path()
124
- self.key = key or load_key(self.key_file)
125
-
126
- # 生效条件:对任意 payload(ctx 为 None 时一并传入 canonical)以 self.key 做 HMAC-SHA256,返回 f"{PREFIX}.{mac.hexdigest()}"。
127
- def sign(self, payload: bytes, ctx: dict = None) -> str:
128
- mac = hmac.new(self.key, canonical(payload, ctx), hashlib.sha256)
129
- return f"{PREFIX}.{mac.hexdigest()}"
130
-
131
- # 生效条件:signature 为假值时按 "" 处理并 strip,不以 PREFIX+"." 开头立即返回 False;否则与 self.sign(payload, ctx) 的结果做 hmac.compare_digest 并返回该布尔值。
132
- def verify(self, payload: bytes, signature: str, ctx: dict = None) -> bool:
133
- sig = (signature or "").strip()
134
- if not sig.startswith(PREFIX + "."):
135
- return False
136
- want = self.sign(payload, ctx)
137
- return hmac.compare_digest(want, sig)
138
-
139
- # 生效条件:无前置;返回 {"name", "kind": "hmac-sha256", "key_file", "note"}——含 key_file 路径但不含密钥材料(密钥本身只经 public() 的 key_id 暴露)。
140
- def describe(self) -> dict:
141
- return {"name": self.name, "kind": "hmac-sha256",
142
- "key_file": self.key_file,
143
- "note": "本地实现,非企业级签名链;无不可否认性"}
144
-
145
- # 生效条件:无必需形参,调用即返回 {"name": self.name, "kind": "hmac-sha256", "key_id": hashlib.sha256(self.key).hexdigest()[:16]}。
146
- def public(self) -> dict:
147
- return {"name": self.name, "kind": "hmac-sha256",
148
- "key_id": hashlib.sha256(self.key).hexdigest()[:16]}
149
-
150
-
151
- _REGISTRY = {
152
- "null": NullSigner,
153
- DEFAULT_SIGNER: HmacLocalSigner,
154
- }
155
- _LOADED_MODULES = set()
156
-
157
-
158
- # 生效条件:payload 为 str 时先按 utf-8 编码,其 base64 文本构成 body['payload'];ctx 非空时以 str 化并按 key 排序后并入 body,最终返回 json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')。
159
- def canonical(payload, ctx: dict = None) -> bytes:
160
- """规范化待签字节串:payload 走 base64,ctx 按 key 排序——避免表示歧义。"""
161
- if isinstance(payload, str):
162
- payload = payload.encode("utf-8")
163
- body = {"payload": base64.b64encode(bytes(payload)).decode("ascii")}
164
- if ctx:
165
- body["ctx"] = {str(k): str(ctx[k]) for k in sorted(ctx)}
166
- return json.dumps(body, ensure_ascii=False, sort_keys=True,
167
- separators=(",", ":")).encode("utf-8")
168
-
169
-
170
- # --------------------------------------------------------------------------
171
- # 注册表(工程侧注入点)
172
- # --------------------------------------------------------------------------
173
-
174
- # 生效条件:name 去空白后非空且 factory 可调用时登记进 _REGISTRY 并返回 {'ok': True, 'name': name, 'signers': sorted(_REGISTRY)},否则抛 SignerError。
175
- def register_signer(name: str, factory) -> dict:
176
- """注册签名器。`factory` 为无参可调用(返回实例)或类本身。"""
177
- name = (name or "").strip()
178
- if not name:
179
- raise SignerError("签名器名不能为空")
180
- if not callable(factory):
181
- raise SignerError("factory 必须可调用(返回 Signer 实例)")
182
- _REGISTRY[name] = factory
183
- return {"ok": True, "name": name, "signers": sorted(_REGISTRY)}
184
-
185
-
186
- # 生效条件:name 已在 _REGISTRY 且不是 'null' 或 DEFAULT_SIGNER 时从注册表移除并返回 {'ok': True, 'signers': sorted(_REGISTRY)},否则抛 SignerError。
187
- def unregister_signer(name: str) -> dict:
188
- if name not in _REGISTRY:
189
- raise SignerError(f"签名器不存在:{name}")
190
- if name in ("null", DEFAULT_SIGNER):
191
- raise SignerError(f"内置签名器不可注销:{name}")
192
- _REGISTRY.pop(name, None)
193
- return {"ok": True, "signers": sorted(_REGISTRY)}
194
-
195
-
196
- # 生效条件:无 required 形参,调用即返回 {'default': default_name(), 'signers': sorted(_REGISTRY)}。
197
- def list_signers() -> dict:
198
- return {"default": default_name(), "signers": sorted(_REGISTRY)}
199
-
200
-
201
- # 生效条件:环境变量 SIGNER_ENV 取值去空白后非空则返回该值,否则返回 DEFAULT_SIGNER。
202
- def default_name() -> str:
203
- return (os.environ.get(SIGNER_ENV) or "").strip() or DEFAULT_SIGNER
204
-
205
-
206
- # 生效条件:环境变量 SIGNER_MODULE_ENV 给出的模块名非空且未加载过时导入该模块并调用其无参 register(),模块加载失败或缺 register() 可调用则抛 SignerError。
207
- def _autoload():
208
- """按 MDCG_SIGNER_MODULE 自动加载工程侧注册模块(进程内只做一次)。"""
209
- mod = (os.environ.get(SIGNER_MODULE_ENV) or "").strip()
210
- if not mod or mod in _LOADED_MODULES:
211
- return
212
- _LOADED_MODULES.add(mod)
213
- try:
214
- m = importlib.import_module(mod)
215
- except Exception as e: # 加载失败不静默:留给调用方判定
216
- raise SignerError(f"签名器模块加载失败 {mod}:{e}")
217
- reg = getattr(m, "register", None)
218
- if not callable(reg):
219
- raise SignerError(f"签名器模块 {mod} 未提供 register()")
220
- reg()
221
-
222
-
223
- # 生效条件:name 去空白后非空(为空取 default_name())且该名已在 _REGISTRY 时构造实例,实例须具备可调用的 sign/verify 否则抛 SignerError;factory 形参含 key_file 时以 key_file 传入构造。
224
- def get_signer(name: str = None, key_file: str = None) -> Signer:
225
- """取签名器实例。未注册即抛错(fail-closed,不静默降级为 null)。"""
226
- _autoload()
227
- nm = (name or "").strip() or default_name()
228
- factory = _REGISTRY.get(nm)
229
- if factory is None:
230
- raise SignerError(
231
- f"签名器未注册:{nm}(已注册:{sorted(_REGISTRY)});"
232
- "工程侧可用 register_signer() 或 MDCG_SIGNER_MODULE 注入")
233
- try:
234
- inst = factory(key_file=key_file) if _accepts_key_file(factory) else factory()
235
- except TypeError:
236
- inst = factory()
237
- if not isinstance(inst, Signer) and not (
238
- callable(getattr(inst, "sign", None))
239
- and callable(getattr(inst, "verify", None))):
240
- raise SignerError(f"签名器 {nm} 未实现 sign/verify 契约")
241
- if not getattr(inst, "name", None):
242
- inst.name = nm
243
- return inst
244
-
245
-
246
- # 生效条件:inspect.signature(factory) 的形参名中含 'key_file' 时返回 True,否则(含 TypeError/ValueError)返回 False。
247
- def _accepts_key_file(factory) -> bool:
248
- try:
249
- import inspect
250
- return "key_file" in inspect.signature(factory).parameters
251
- except (TypeError, ValueError):
252
- return False
253
-
254
-
255
- # --------------------------------------------------------------------------
256
- # 密钥(仅内置 hmac-local 需要;工程侧自行管理私钥)
257
- # --------------------------------------------------------------------------
258
-
259
- # 生效条件:显式 path 非空即返回该 path,否则返回环境变量 KEY_FILE_ENV 的值,两者皆空返回 DEFAULT_KEY_FILE。
260
- def key_file_path(path: str = None) -> str:
261
- return path or os.environ.get(KEY_FILE_ENV) or DEFAULT_KEY_FILE
262
-
263
-
264
- # 生效条件:key_file_path(path) 处可读出非空内容时返回该内容 bytes,否则生成 secrets.token_urlsafe(32) 密钥、经 save_key 落盘后返回该密钥。
265
- def load_key(path: str = None) -> bytes:
266
- p = key_file_path(path)
267
- if os.path.exists(p):
268
- with open(p, "rb") as f:
269
- raw = f.read().strip()
270
- if raw:
271
- return raw
272
- key = secrets.token_urlsafe(32).encode("utf-8")
273
- save_key(key, p)
274
- return key
275
-
276
-
277
- # 生效条件:key 为 bytes 时原样写、否则 str(key).encode('utf-8'),写入 key_file_path(path) 同目录的临时文件后 os.replace 覆盖到该路径并返回它。
278
- def save_key(key: bytes, path: str = None) -> str:
279
- p = key_file_path(path)
280
- os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
281
- tmp = p + ".tmp"
282
- with open(tmp, "wb") as f:
283
- f.write(key if isinstance(key, bytes) else str(key).encode("utf-8"))
284
- try:
285
- os.chmod(tmp, 0o600)
286
- except OSError:
287
- pass
288
- os.replace(tmp, p)
289
- return p
290
-
291
-
292
- # --------------------------------------------------------------------------
293
- # 子系统签名策略(D-4:智能体自行决定子系统怎么用签名链)
294
- # --------------------------------------------------------------------------
295
-
296
- # 生效条件:无参无外部依赖;返回默认策略 {"signer": None, "sign_on": ["handshake"], "require_peer_signature": False, "on_verify_fail": "degrade"}(调用方需经 policy() 归一化后再用);
297
- def _default_policy() -> dict:
298
- return {"signer": None, "sign_on": ["handshake"],
299
- "require_peer_signature": False, "on_verify_fail": "degrade"}
300
-
301
-
302
- # 生效条件:pol 中 'signer'/'sign_on'/'require_peer_signature'/'on_verify_fail' 键覆盖默认策略,sign_on 只保留属于 SIGN_ACTIONS 的项,on_verify_fail 不在 ON_VERIFY_FAIL 时回落 'degrade'。
303
- def _normalize(pol: dict) -> dict:
304
- out = _default_policy()
305
- out.update({k: pol[k] for k in ("signer", "sign_on",
306
- "require_peer_signature",
307
- "on_verify_fail") if k in pol})
308
- on = out.get("sign_on")
309
- if isinstance(on, str):
310
- on = [x for x in on.split(",") if x.strip()]
311
- out["sign_on"] = [x for x in (on or []) if x in SIGN_ACTIONS]
312
- if out.get("on_verify_fail") not in ON_VERIFY_FAIL:
313
- out["on_verify_fail"] = "degrade"
314
- out["require_peer_signature"] = bool(out.get("require_peer_signature"))
315
- return out
316
-
317
-
318
- # 生效条件:显式 path 非空即返回该 path,否则返回环境变量 SIGNERS_FILE_ENV 的值,两者皆空返回 DEFAULT_SIGNERS_FILE。
319
- def signers_file(path: str = None) -> str:
320
- return path or os.environ.get(SIGNERS_FILE_ENV) or DEFAULT_SIGNERS_FILE
321
-
322
-
323
- # 生效条件:signers_file(path) 处可读出 dict 时补上缺省 schema/subsystems 后返回该 dict,否则返回 {'schema': SCHEMA, 'default_signer': DEFAULT_SIGNER, 'subsystems': {}, 'updated_at': None}。
324
- def load_policies(path: str = None) -> dict:
325
- p = signers_file(path)
326
- if os.path.exists(p):
327
- try:
328
- with open(p, "r", encoding="utf-8") as f:
329
- d = json.load(f)
330
- if isinstance(d, dict):
331
- d.setdefault("schema", SCHEMA)
332
- d.setdefault("subsystems", {})
333
- return d
334
- except (OSError, ValueError):
335
- pass
336
- return {"schema": SCHEMA, "default_signer": DEFAULT_SIGNER,
337
- "subsystems": {}, "updated_at": None}
338
-
339
-
340
- # 生效条件:data 传入任意映射即被 dict(data) 浅拷贝并覆写 schema/updated_at 后写入 signers_file(path)(path 为假值时由 signers_file(path) 决定路径)。
341
- def save_policies(data: dict, path: str = None) -> str:
342
- p = signers_file(path)
343
- os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
344
- data = dict(data)
345
- data["schema"] = SCHEMA
346
- data["updated_at"] = time.time()
347
- tmp = p + ".tmp"
348
- with open(tmp, "w", encoding="utf-8") as f:
349
- json.dump(data, f, ensure_ascii=False, indent=1, sort_keys=True)
350
- try:
351
- os.chmod(tmp, 0o600)
352
- except OSError:
353
- pass
354
- os.replace(tmp, p)
355
- return p
356
-
357
-
358
- # 生效条件:subsystem 为假值(None/空串)时按空串键在 d.get("subsystems") or {} 中查策略,取不到则用默认策略,且 out["signer"] 为假值时回落 d.get("default_signer") or DEFAULT_SIGNER。
359
- def policy_for(subsystem: str = None, path: str = None) -> dict:
360
- """取子系统策略;未声明则用默认(`default_signer` + handshake 签名)。"""
361
- d = load_policies(path)
362
- pol = (d.get("subsystems") or {}).get(subsystem or "") or {}
363
- out = _normalize(pol)
364
- if not out.get("signer"):
365
- out["signer"] = d.get("default_signer") or DEFAULT_SIGNER
366
- out["subsystem"] = subsystem or ""
367
- return out
368
-
369
-
370
- # 生效条件:subsystem 为假值(None/空串)时抛 SignerError("子系统名不能为空");否则 kw 中非 None 项合并进该子系统策略并落盘,且 kw.get("default_signer") 为真值时同时更新 d["default_signer"]。
371
- def set_policy(subsystem: str, path: str = None, **kw) -> dict:
372
- """声明/更新子系统签名策略——**这是智能体行使 D-4 的入口**。"""
373
- if not subsystem:
374
- raise SignerError("子系统名不能为空")
375
- d = load_policies(path)
376
- cur = (d["subsystems"].get(subsystem) or {})
377
- cur.update({k: v for k, v in kw.items() if v is not None})
378
- d["subsystems"][subsystem] = _normalize(cur)
379
- if kw.get("default_signer"):
380
- d["default_signer"] = kw["default_signer"]
381
- save_policies(d, path)
382
- return {"ok": True, "subsystem": subsystem,
383
- "policy": policy_for(subsystem, path),
384
- "file": signers_file(path)}
385
-
386
-
387
- # 生效条件:subsystem 命中 d.get("subsystems") 的键(含空串)时才弹出并 save_policies 落盘;未命中则不写盘,两种情况都返回 ok=True 与 sorted(d.get("subsystems") or {})。
388
- def remove_policy(subsystem: str, path: str = None) -> dict:
389
- d = load_policies(path)
390
- if subsystem in (d.get("subsystems") or {}):
391
- d["subsystems"].pop(subsystem)
392
- save_policies(d, path)
393
- return {"ok": True, "subsystem": subsystem,
394
- "subsystems": sorted(d.get("subsystems") or {})}
395
-
396
-
397
- # --------------------------------------------------------------------------
398
- # 策略驱动的签名 / 验签(连接层调用)
399
- # --------------------------------------------------------------------------
400
-
401
- # 生效条件:action 不在 policy_for(subsystem, path)["sign_on"] 中时返回 signed=False、signer=None、signature="";在列内时由 get_signer(pol.get("signer")) 对 payload 签名并返回 signed=True 与 s.public()。
402
- def sign_for(subsystem: str, payload, ctx: dict = None,
403
- action: str = "handshake", path: str = None) -> dict:
404
- """按子系统策略签名。策略未把该 action 列入 `sign_on` 则**不签**。"""
405
- pol = policy_for(subsystem, path)
406
- if action not in pol["sign_on"]:
407
- return {"ok": True, "signed": False, "action": action,
408
- "signer": None, "signature": "",
409
- "reason": f"子系统 {subsystem or '(默认)'} 策略未对 {action} 要求签名"}
410
- s = get_signer(pol.get("signer"))
411
- sig = s.sign(payload, ctx)
412
- return {"ok": True, "signed": True, "action": action,
413
- "signer": getattr(s, "name", None), "signature": sig,
414
- "public": s.public()}
415
-
416
-
417
- # 生效条件:required=bool(pol["require_peer_signature"]),signature 经 (signature or "").strip() 后为空且 required 为假时返回 ok=True/required=False,required 为真且为空时返回 ok=False/missing=True/on_fail="reject",签名非空时由 get_signer(pol.get("signer")) 验签、异常置 ok=False 且 err=str(e),失败时 on_fail 取 pol["on_verify_fail"]。
418
- def verify_for(subsystem: str, payload, signature, ctx: dict = None,
419
- action: str = "handshake", path: str = None) -> dict:
420
- """按子系统策略验对端签名。
421
-
422
- 语义分工(避免歧义):
423
- · `sign_on` —— **本节点**在哪些动作上签名(见 `sign_for`);
424
- · `require_peer_signature` —— **对端**是否必须提供签名。
425
-
426
- 三种结果:
427
- · 不要求且未提供 → `ok=True, required=False`(放行);
428
- · 要求但缺失 → `ok=False, missing=True, on_fail="reject"`(fail-closed);
429
- · 提供了就验 → 失败时 `on_fail` 取策略值,由调用方执行处置。
430
- """
431
- pol = policy_for(subsystem, path)
432
- required = bool(pol["require_peer_signature"])
433
- sig = (signature or "").strip()
434
-
435
- if not required and not sig:
436
- return {"ok": True, "required": False, "missing": False,
437
- "action": action, "signer": None, "on_fail": None,
438
- "require_peer_signature": False,
439
- "reason": "策略未要求对端签名,且对端未提供"}
440
- if required and not sig:
441
- return {"ok": False, "required": True, "missing": True,
442
- "action": action, "signer": None, "on_fail": "reject",
443
- "require_peer_signature": True,
444
- "reason": "策略要求对端签名,但对端未提供"}
445
-
446
- s = get_signer(pol.get("signer"))
447
- try:
448
- ok = bool(s.verify(payload, sig, ctx))
449
- except Exception as e:
450
- ok, err = False, str(e)
451
- else:
452
- err = ""
453
- return {"ok": ok, "required": required, "missing": False, "action": action,
454
- "signer": getattr(s, "name", None),
455
- "on_fail": None if ok else pol["on_verify_fail"],
456
- "require_peer_signature": required,
457
- "reason": "" if ok else (err or "验签失败")}
458
-
459
-
460
- # --------------------------------------------------------------------------
461
- # 自描述 / CLI
462
- # --------------------------------------------------------------------------
463
-
464
- # 生效条件:无必需形参,default_signer 取 d.get("default_signer") or DEFAULT_SIGNER,subsystems 缺失或为假值时按 {} 处理并逐个 _normalize,updated_at 取 d.get("updated_at")。
465
- def show(path: str = None) -> dict:
466
- d = load_policies(path)
467
- return {"file": signers_file(path),
468
- "default_signer": d.get("default_signer") or DEFAULT_SIGNER,
469
- "subsystems": {k: _normalize(v) for k, v in
470
- (d.get("subsystems") or {}).items()},
471
- "updated_at": d.get("updated_at")}
472
-
473
-
474
- # 生效条件:无必需形参,返回 SIGN_ACTIONS、ON_VERIFY_FAIL 的列表、list_signers()、_REGISTRY 中名为 "null" 或 DEFAULT_SIGNER 的内置签名器 describe(),以及 SIGNER_ENV/SIGNER_MODULE_ENV/SIGNERS_FILE_ENV/KEY_FILE_ENV 常量。
475
- def catalog() -> dict:
476
- return {
477
- "layer": "签名接口(蜂群互联 D-4)",
478
- "decision": "协议内核只定义契约;实现落工程项目;子系统自行决定用法",
479
- "actions": list(SIGN_ACTIONS),
480
- "on_verify_fail": list(ON_VERIFY_FAIL),
481
- "signers": list_signers(),
482
- "builtin": {n: _REGISTRY[n]().describe() for n in sorted(_REGISTRY)
483
- if n in ("null", DEFAULT_SIGNER)},
484
- "policy_file": signers_file(),
485
- "env": {"signer": SIGNER_ENV, "module": SIGNER_MODULE_ENV,
486
- "policy_file": SIGNERS_FILE_ENV, "key_file": KEY_FILE_ENV},
487
- }
488
-
489
-
490
- # 生效条件:obj 为任意对象时以 json.dumps(ensure_ascii=False, indent=1, default=str) 打印(不可序列化值经 default=str 转换),源码无返回值。
491
- def _print(obj):
492
- print(json.dumps(obj, ensure_ascii=False, indent=1, default=str))
493
-
494
-
495
- # 生效条件:argv(默认取 sys.argv)经 argparse 解析出必填 cmd(catalog/list/show/set/rm/sign/verify 之一)后分派到对应分支;任一分支抛出 SignerError 时向 stderr 打印并返回 2,否则返回 0。
496
- def main(argv=None):
497
- ap = argparse.ArgumentParser(
498
- prog="python -m md_cg.signer",
499
- description="签名接口层:契约 + 子系统策略(D-4:内核不实现密码学)")
500
- ap.add_argument("--signers-file", default=None,
501
- help="策略文件(默认 ~/.mdcg/_signers.json)")
502
- ap.add_argument("--key-file", default=None,
503
- help="本地密钥文件(默认 ~/.mdcg/_signer.key)")
504
- sub = ap.add_subparsers(dest="cmd", required=True)
505
-
506
- sub.add_parser("catalog", help="签名接口自描述")
507
- sub.add_parser("list", help="已注册签名器")
508
- sub.add_parser("show", help="子系统签名策略")
509
-
510
- p_set = sub.add_parser("set", help="声明子系统签名策略")
511
- p_set.add_argument("subsystem")
512
- p_set.add_argument("--signer", default=None)
513
- p_set.add_argument("--sign-on", default=None, help="逗号分隔:handshake,evidence")
514
- p_set.add_argument("--require-peer", action="store_true", default=None)
515
- p_set.add_argument("--on-verify-fail", default=None,
516
- choices=list(ON_VERIFY_FAIL))
517
-
518
- p_rm = sub.add_parser("rm", help="删除子系统策略")
519
- p_rm.add_argument("subsystem")
520
-
521
- p_s = sub.add_parser("sign", help="按策略签名(payload 取自 --payload 或 stdin)")
522
- p_s.add_argument("--subsystem", default=None)
523
- p_s.add_argument("--action", default="handshake", choices=list(SIGN_ACTIONS))
524
- p_s.add_argument("--payload", default=None)
525
-
526
- p_v = sub.add_parser("verify", help="按策略验签")
527
- p_v.add_argument("--subsystem", default=None)
528
- p_v.add_argument("--action", default="handshake", choices=list(SIGN_ACTIONS))
529
- p_v.add_argument("--payload", required=True)
530
- p_v.add_argument("--signature", required=True)
531
-
532
- a = ap.parse_args(argv)
533
- try:
534
- if a.cmd == "catalog":
535
- _print(catalog())
536
- elif a.cmd == "list":
537
- _print(list_signers())
538
- elif a.cmd == "show":
539
- _print(show(a.signers_file))
540
- elif a.cmd == "set":
541
- on = ([x for x in a.sign_on.split(",") if x.strip()]
542
- if a.sign_on else None)
543
- _print(set_policy(a.subsystem, path=a.signers_file,
544
- signer=a.signer, sign_on=on,
545
- require_peer_signature=a.require_peer,
546
- on_verify_fail=a.on_verify_fail))
547
- elif a.cmd == "rm":
548
- _print(remove_policy(a.subsystem, a.signers_file))
549
- elif a.cmd == "sign":
550
- payload = a.payload if a.payload is not None else sys.stdin.read()
551
- _print(sign_for(a.subsystem, payload, action=a.action,
552
- path=a.signers_file))
553
- elif a.cmd == "verify":
554
- _print(verify_for(a.subsystem, a.payload, a.signature,
555
- action=a.action, path=a.signers_file))
556
- except SignerError as e:
557
- print(f"[md_cg.signer] {e}", file=sys.stderr)
558
- return 2
559
- return 0
560
-
561
-
562
- if __name__ == "__main__":
1
+ # -*- coding: utf-8 -*-
2
+ """签名接口层(蜂群互联 D-4)。
3
+
4
+ 设计裁决 D-4(2026-09-10,设计者):
5
+
6
+ · 协议内核**不实现密码学**,只定义 `sign` / `verify` 契约;
7
+ · 具体签名链(企业 CA / KMS / 签章服务)在**工程项目**中实现;
8
+ · **搭载灵枢的智能体自行决定子系统如何使用签名链**(策略落盘 `_signers.json`)。
9
+
10
+ 因此本模块只做三件事:
11
+ ① 定义可插拔的 `Signer` 契约 + 注册表;
12
+ ② 提供两个最简内置实现(`null` / `hmac-local`);
13
+ ③ 读取并执行**子系统签名策略**(何时签、是否要求对端签、验签失败如何处置)。
14
+
15
+ **内核不保存私钥**:私钥归工程侧,这里只持有 `sign` / `verify` 的可调用引用。
16
+ 工程侧注入方式:
17
+
18
+ # 1) 代码注入
19
+ from md_cg.signer import register_signer
20
+ register_signer("corp_pki", lambda: MyCorpSigner(...))
21
+
22
+ # 2) 启动自动加载(MCP 子进程推荐)
23
+ MDCG_SIGNER_MODULE=mycorp.signing # 模块内提供 register()
24
+
25
+ 零第三方依赖。
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import base64
31
+ import hashlib
32
+ import hmac
33
+ import importlib
34
+ import json
35
+ import os
36
+ import secrets
37
+ import sys
38
+ import time
39
+
40
+ from .datapath import aux_root
41
+ from .fsutil import publish
42
+
43
+ SIGNER_ENV = "MDCG_SIGNER"
44
+ SIGNER_MODULE_ENV = "MDCG_SIGNER_MODULE"
45
+ SIGNERS_FILE_ENV = "MDCG_SIGNERS_FILE"
46
+ KEY_FILE_ENV = "MDCG_SIGNER_KEY_FILE"
47
+
48
+ DEFAULT_DIR = aux_root()
49
+ DEFAULT_SIGNERS_FILE = os.path.join(DEFAULT_DIR, "_signers.json")
50
+ DEFAULT_KEY_FILE = os.path.join(DEFAULT_DIR, "_signer.key")
51
+
52
+ SCHEMA = 1
53
+ DEFAULT_SIGNER = "hmac-local"
54
+ PREFIX = "hmac1"
55
+
56
+ # 需要签名的动作(子系统策略里引用)
57
+ SIGN_ACTIONS = ("handshake", "evidence", "withdrawal")
58
+ # 验签失败的处置档位(宪章第七条降级为默认)
59
+ ON_VERIFY_FAIL = ("degrade", "isolate", "reject")
60
+
61
+
62
+ class SignerError(Exception):
63
+ """签名接口层错误。"""
64
+
65
+
66
+ # --------------------------------------------------------------------------
67
+ # 契约与内置实现
68
+ # --------------------------------------------------------------------------
69
+
70
+ # 生效条件:实现方提供可调用的 sign(payload, ctx) 与 verify(payload, signature, ctx) 即满足鸭子类型契约;name 缺省为 'abstract',基类实现直接抛 NotImplementedError。
71
+ class Signer:
72
+ """签名器契约。工程侧实现只需满足这两个方法(鸭子类型即可)。"""
73
+
74
+ name = "abstract"
75
+
76
+ # 生效条件:基类实现,任意 payload 下直接 raise NotImplementedError,由子类覆写。
77
+ def sign(self, payload: bytes, ctx: dict = None) -> str:
78
+ raise NotImplementedError
79
+
80
+ # 生效条件:基类实现,任意 payload 与 signature 下直接 raise NotImplementedError,由子类覆写。
81
+ def verify(self, payload: bytes, signature: str, ctx: dict = None) -> bool:
82
+ raise NotImplementedError
83
+
84
+ # 生效条件:无前置;返回 {"name": self.name, "kind": "abstract"},不含密钥材料,供注册表自描述用。
85
+ def describe(self) -> dict:
86
+ return {"name": self.name, "kind": "abstract"}
87
+
88
+ # 生效条件:无 required 形参,调用即返回仅含 {'name': self.name} 的 dict,不含密钥材料。
89
+ def public(self) -> dict:
90
+ """可对外公开的验证信息(**不得含密钥**)。"""
91
+ return {"name": self.name}
92
+
93
+
94
+ # 生效条件:sign 对任意 payload 恒返回 '',verify 仅当 signature 为 '' 或 None 时返回 True(name 为 'null')。
95
+ class NullSigner(Signer):
96
+ """不签名(观察期 / 纯本地)。验签时把「空签名」视为通过。"""
97
+
98
+ name = "null"
99
+
100
+ # 生效条件:任意 payload 下均返回空串 "",不做任何签名计算。
101
+ def sign(self, payload: bytes, ctx: dict = None) -> str:
102
+ return ""
103
+
104
+ # 生效条件:signature 恰为 "" 或 None 时返回 True,其余取值(含仅含空白的字符串)返回 False,与 payload 无关。
105
+ def verify(self, payload: bytes, signature: str, ctx: dict = None) -> bool:
106
+ return signature in ("", None)
107
+
108
+ # 生效条件:无前置;返回 {"name", "kind": "none", "note"},其中 note 明示「不签名;仅用于观察期或纯本地场景」,不含密钥材料。
109
+ def describe(self) -> dict:
110
+ return {"name": self.name, "kind": "none",
111
+ "note": "不签名;仅用于观察期或纯本地场景"}
112
+
113
+
114
+ # 生效条件:__init__ 无必需形参;key_file 为假值(None/空串)时回落 key_file_path(),key 为假值(None/空串)时回落 load_key(self.key_file)。
115
+ class HmacLocalSigner(Signer):
116
+ """本地 HMAC-SHA256 实现——与 `_tokens.json` 同一信任根级别的最简方案。
117
+
118
+ 它**不是**企业级签名链(无不可否认性、无私钥托管),
119
+ 仅用于单机自测与「有签名位置但暂无 CA」的过渡期。
120
+ """
121
+
122
+ name = "hmac-local"
123
+
124
+ # 生效条件:key_file 为假值(None/空串)时 self.key_file 回落 key_file_path();key 为假值(None/空字节串)时 self.key 回落 load_key(self.key_file)。
125
+ def __init__(self, key: bytes = None, key_file: str = None):
126
+ self.key_file = key_file or key_file_path()
127
+ self.key = key or load_key(self.key_file)
128
+
129
+ # 生效条件:对任意 payload(ctx 为 None 时一并传入 canonical)以 self.key 做 HMAC-SHA256,返回 f"{PREFIX}.{mac.hexdigest()}"。
130
+ def sign(self, payload: bytes, ctx: dict = None) -> str:
131
+ mac = hmac.new(self.key, canonical(payload, ctx), hashlib.sha256)
132
+ return f"{PREFIX}.{mac.hexdigest()}"
133
+
134
+ # 生效条件:signature 为假值时按 "" 处理并 strip,不以 PREFIX+"." 开头立即返回 False;否则与 self.sign(payload, ctx) 的结果做 hmac.compare_digest 并返回该布尔值。
135
+ def verify(self, payload: bytes, signature: str, ctx: dict = None) -> bool:
136
+ sig = (signature or "").strip()
137
+ if not sig.startswith(PREFIX + "."):
138
+ return False
139
+ want = self.sign(payload, ctx)
140
+ return hmac.compare_digest(want, sig)
141
+
142
+ # 生效条件:无前置;返回 {"name", "kind": "hmac-sha256", "key_file", "note"}——含 key_file 路径但不含密钥材料(密钥本身只经 public() 的 key_id 暴露)。
143
+ def describe(self) -> dict:
144
+ return {"name": self.name, "kind": "hmac-sha256",
145
+ "key_file": self.key_file,
146
+ "note": "本地实现,非企业级签名链;无不可否认性"}
147
+
148
+ # 生效条件:无必需形参,调用即返回 {"name": self.name, "kind": "hmac-sha256", "key_id": hashlib.sha256(self.key).hexdigest()[:16]}。
149
+ def public(self) -> dict:
150
+ return {"name": self.name, "kind": "hmac-sha256",
151
+ "key_id": hashlib.sha256(self.key).hexdigest()[:16]}
152
+
153
+
154
+ _REGISTRY = {
155
+ "null": NullSigner,
156
+ DEFAULT_SIGNER: HmacLocalSigner,
157
+ }
158
+ _LOADED_MODULES = set()
159
+
160
+
161
+ # 生效条件:payload 为 str 时先按 utf-8 编码,其 base64 文本构成 body['payload'];ctx 非空时以 str 化并按 key 排序后并入 body,最终返回 json.dumps(body, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')。
162
+ def canonical(payload, ctx: dict = None) -> bytes:
163
+ """规范化待签字节串:payload 走 base64,ctx 按 key 排序——避免表示歧义。"""
164
+ if isinstance(payload, str):
165
+ payload = payload.encode("utf-8")
166
+ body = {"payload": base64.b64encode(bytes(payload)).decode("ascii")}
167
+ if ctx:
168
+ body["ctx"] = {str(k): str(ctx[k]) for k in sorted(ctx)}
169
+ return json.dumps(body, ensure_ascii=False, sort_keys=True,
170
+ separators=(",", ":")).encode("utf-8")
171
+
172
+
173
+ # --------------------------------------------------------------------------
174
+ # 注册表(工程侧注入点)
175
+ # --------------------------------------------------------------------------
176
+
177
+ # 生效条件:name 去空白后非空且 factory 可调用时登记进 _REGISTRY 并返回 {'ok': True, 'name': name, 'signers': sorted(_REGISTRY)},否则抛 SignerError。
178
+ def register_signer(name: str, factory) -> dict:
179
+ """注册签名器。`factory` 为无参可调用(返回实例)或类本身。"""
180
+ name = (name or "").strip()
181
+ if not name:
182
+ raise SignerError("签名器名不能为空")
183
+ if not callable(factory):
184
+ raise SignerError("factory 必须可调用(返回 Signer 实例)")
185
+ _REGISTRY[name] = factory
186
+ return {"ok": True, "name": name, "signers": sorted(_REGISTRY)}
187
+
188
+
189
+ # 生效条件:name 已在 _REGISTRY 且不是 'null' 或 DEFAULT_SIGNER 时从注册表移除并返回 {'ok': True, 'signers': sorted(_REGISTRY)},否则抛 SignerError。
190
+ def unregister_signer(name: str) -> dict:
191
+ if name not in _REGISTRY:
192
+ raise SignerError(f"签名器不存在:{name}")
193
+ if name in ("null", DEFAULT_SIGNER):
194
+ raise SignerError(f"内置签名器不可注销:{name}")
195
+ _REGISTRY.pop(name, None)
196
+ return {"ok": True, "signers": sorted(_REGISTRY)}
197
+
198
+
199
+ # 生效条件:无 required 形参,调用即返回 {'default': default_name(), 'signers': sorted(_REGISTRY)}。
200
+ def list_signers() -> dict:
201
+ return {"default": default_name(), "signers": sorted(_REGISTRY)}
202
+
203
+
204
+ # 生效条件:环境变量 SIGNER_ENV 取值去空白后非空则返回该值,否则返回 DEFAULT_SIGNER。
205
+ def default_name() -> str:
206
+ return (os.environ.get(SIGNER_ENV) or "").strip() or DEFAULT_SIGNER
207
+
208
+
209
+ # 生效条件:环境变量 SIGNER_MODULE_ENV 给出的模块名非空且未加载过时导入该模块并调用其无参 register(),模块加载失败或缺 register() 可调用则抛 SignerError。
210
+ def _autoload():
211
+ """按 MDCG_SIGNER_MODULE 自动加载工程侧注册模块(进程内只做一次)。"""
212
+ mod = (os.environ.get(SIGNER_MODULE_ENV) or "").strip()
213
+ if not mod or mod in _LOADED_MODULES:
214
+ return
215
+ _LOADED_MODULES.add(mod)
216
+ try:
217
+ m = importlib.import_module(mod)
218
+ except Exception as e: # 加载失败不静默:留给调用方判定
219
+ raise SignerError(f"签名器模块加载失败 {mod}:{e}")
220
+ reg = getattr(m, "register", None)
221
+ if not callable(reg):
222
+ raise SignerError(f"签名器模块 {mod} 未提供 register()")
223
+ reg()
224
+
225
+
226
+ # 生效条件:name 去空白后非空(为空取 default_name())且该名已在 _REGISTRY 时构造实例,实例须具备可调用的 sign/verify 否则抛 SignerError;factory 形参含 key_file 时以 key_file 传入构造。
227
+ def get_signer(name: str = None, key_file: str = None) -> Signer:
228
+ """取签名器实例。未注册即抛错(fail-closed,不静默降级为 null)。"""
229
+ _autoload()
230
+ nm = (name or "").strip() or default_name()
231
+ factory = _REGISTRY.get(nm)
232
+ if factory is None:
233
+ raise SignerError(
234
+ f"签名器未注册:{nm}(已注册:{sorted(_REGISTRY)});"
235
+ "工程侧可用 register_signer() 或 MDCG_SIGNER_MODULE 注入")
236
+ try:
237
+ inst = factory(key_file=key_file) if _accepts_key_file(factory) else factory()
238
+ except TypeError:
239
+ inst = factory()
240
+ if not isinstance(inst, Signer) and not (
241
+ callable(getattr(inst, "sign", None))
242
+ and callable(getattr(inst, "verify", None))):
243
+ raise SignerError(f"签名器 {nm} 未实现 sign/verify 契约")
244
+ if not getattr(inst, "name", None):
245
+ inst.name = nm
246
+ return inst
247
+
248
+
249
+ # 生效条件:inspect.signature(factory) 的形参名中含 'key_file' 时返回 True,否则(含 TypeError/ValueError)返回 False。
250
+ def _accepts_key_file(factory) -> bool:
251
+ try:
252
+ import inspect
253
+ return "key_file" in inspect.signature(factory).parameters
254
+ except (TypeError, ValueError):
255
+ return False
256
+
257
+
258
+ # --------------------------------------------------------------------------
259
+ # 密钥(仅内置 hmac-local 需要;工程侧自行管理私钥)
260
+ # --------------------------------------------------------------------------
261
+
262
+ # 生效条件:显式 path 非空即返回该 path,否则返回环境变量 KEY_FILE_ENV 的值,两者皆空返回 DEFAULT_KEY_FILE。
263
+ def key_file_path(path: str = None) -> str:
264
+ return path or os.environ.get(KEY_FILE_ENV) or DEFAULT_KEY_FILE
265
+
266
+
267
+ # 生效条件:key_file_path(path) 处可读出非空内容时返回该内容 bytes,否则生成 secrets.token_urlsafe(32) 密钥、经 save_key 落盘后返回该密钥。
268
+ def load_key(path: str = None) -> bytes:
269
+ p = key_file_path(path)
270
+ if os.path.exists(p):
271
+ with open(p, "rb") as f:
272
+ raw = f.read().strip()
273
+ if raw:
274
+ return raw
275
+ key = secrets.token_urlsafe(32).encode("utf-8")
276
+ save_key(key, p)
277
+ return key
278
+
279
+
280
+ # 生效条件:key 为 bytes 时原样写、否则 str(key).encode('utf-8'),写入 key_file_path(path) 同目录的临时文件后 publish(带 Windows 短重试的 os.replace)覆盖到该路径并返回它。
281
+ def save_key(key: bytes, path: str = None) -> str:
282
+ p = key_file_path(path)
283
+ os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
284
+ tmp = p + ".tmp"
285
+ with open(tmp, "wb") as f:
286
+ f.write(key if isinstance(key, bytes) else str(key).encode("utf-8"))
287
+ try:
288
+ os.chmod(tmp, 0o600)
289
+ except OSError:
290
+ pass
291
+ publish(tmp, p)
292
+ return p
293
+
294
+
295
+ # --------------------------------------------------------------------------
296
+ # 子系统签名策略(D-4:智能体自行决定子系统怎么用签名链)
297
+ # --------------------------------------------------------------------------
298
+
299
+ # 生效条件:无参无外部依赖;返回默认策略 {"signer": None, "sign_on": ["handshake"], "require_peer_signature": False, "on_verify_fail": "degrade"}(调用方需经 policy() 归一化后再用);
300
+ def _default_policy() -> dict:
301
+ return {"signer": None, "sign_on": ["handshake"],
302
+ "require_peer_signature": False, "on_verify_fail": "degrade"}
303
+
304
+
305
+ # 生效条件:pol 中 'signer'/'sign_on'/'require_peer_signature'/'on_verify_fail' 键覆盖默认策略,sign_on 只保留属于 SIGN_ACTIONS 的项,on_verify_fail 不在 ON_VERIFY_FAIL 时回落 'degrade'。
306
+ def _normalize(pol: dict) -> dict:
307
+ out = _default_policy()
308
+ out.update({k: pol[k] for k in ("signer", "sign_on",
309
+ "require_peer_signature",
310
+ "on_verify_fail") if k in pol})
311
+ on = out.get("sign_on")
312
+ if isinstance(on, str):
313
+ on = [x for x in on.split(",") if x.strip()]
314
+ out["sign_on"] = [x for x in (on or []) if x in SIGN_ACTIONS]
315
+ if out.get("on_verify_fail") not in ON_VERIFY_FAIL:
316
+ out["on_verify_fail"] = "degrade"
317
+ out["require_peer_signature"] = bool(out.get("require_peer_signature"))
318
+ return out
319
+
320
+
321
+ # 生效条件:显式 path 非空即返回该 path,否则返回环境变量 SIGNERS_FILE_ENV 的值,两者皆空返回 DEFAULT_SIGNERS_FILE。
322
+ def signers_file(path: str = None) -> str:
323
+ return path or os.environ.get(SIGNERS_FILE_ENV) or DEFAULT_SIGNERS_FILE
324
+
325
+
326
+ # 生效条件:signers_file(path) 处可读出 dict 时补上缺省 schema/subsystems 后返回该 dict,否则返回 {'schema': SCHEMA, 'default_signer': DEFAULT_SIGNER, 'subsystems': {}, 'updated_at': None}。
327
+ def load_policies(path: str = None) -> dict:
328
+ p = signers_file(path)
329
+ if os.path.exists(p):
330
+ try:
331
+ with open(p, "r", encoding="utf-8") as f:
332
+ d = json.load(f)
333
+ if isinstance(d, dict):
334
+ d.setdefault("schema", SCHEMA)
335
+ d.setdefault("subsystems", {})
336
+ return d
337
+ except (OSError, ValueError):
338
+ pass
339
+ return {"schema": SCHEMA, "default_signer": DEFAULT_SIGNER,
340
+ "subsystems": {}, "updated_at": None}
341
+
342
+
343
+ # 生效条件:data 传入任意映射即被 dict(data) 浅拷贝并覆写 schema/updated_at 后写入 signers_file(path)(path 为假值时由 signers_file(path) 决定路径)。
344
+ def save_policies(data: dict, path: str = None) -> str:
345
+ p = signers_file(path)
346
+ os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
347
+ data = dict(data)
348
+ data["schema"] = SCHEMA
349
+ data["updated_at"] = time.time()
350
+ tmp = p + ".tmp"
351
+ with open(tmp, "w", encoding="utf-8") as f:
352
+ json.dump(data, f, ensure_ascii=False, indent=1, sort_keys=True)
353
+ try:
354
+ os.chmod(tmp, 0o600)
355
+ except OSError:
356
+ pass
357
+ publish(tmp, p)
358
+ return p
359
+
360
+
361
+ # 生效条件:subsystem 为假值(None/空串)时按空串键在 d.get("subsystems") or {} 中查策略,取不到则用默认策略,且 out["signer"] 为假值时回落 d.get("default_signer") or DEFAULT_SIGNER。
362
+ def policy_for(subsystem: str = None, path: str = None) -> dict:
363
+ """取子系统策略;未声明则用默认(`default_signer` + handshake 签名)。"""
364
+ d = load_policies(path)
365
+ pol = (d.get("subsystems") or {}).get(subsystem or "") or {}
366
+ out = _normalize(pol)
367
+ if not out.get("signer"):
368
+ out["signer"] = d.get("default_signer") or DEFAULT_SIGNER
369
+ out["subsystem"] = subsystem or ""
370
+ return out
371
+
372
+
373
+ # 生效条件:subsystem 为假值(None/空串)时抛 SignerError("子系统名不能为空");否则 kw 中非 None 项合并进该子系统策略并落盘,且 kw.get("default_signer") 为真值时同时更新 d["default_signer"]。
374
+ def set_policy(subsystem: str, path: str = None, **kw) -> dict:
375
+ """声明/更新子系统签名策略——**这是智能体行使 D-4 的入口**。"""
376
+ if not subsystem:
377
+ raise SignerError("子系统名不能为空")
378
+ d = load_policies(path)
379
+ cur = (d["subsystems"].get(subsystem) or {})
380
+ cur.update({k: v for k, v in kw.items() if v is not None})
381
+ d["subsystems"][subsystem] = _normalize(cur)
382
+ if kw.get("default_signer"):
383
+ d["default_signer"] = kw["default_signer"]
384
+ save_policies(d, path)
385
+ return {"ok": True, "subsystem": subsystem,
386
+ "policy": policy_for(subsystem, path),
387
+ "file": signers_file(path)}
388
+
389
+
390
+ # 生效条件:subsystem 命中 d.get("subsystems") 的键(含空串)时才弹出并 save_policies 落盘;未命中则不写盘,两种情况都返回 ok=True 与 sorted(d.get("subsystems") or {})。
391
+ def remove_policy(subsystem: str, path: str = None) -> dict:
392
+ d = load_policies(path)
393
+ if subsystem in (d.get("subsystems") or {}):
394
+ d["subsystems"].pop(subsystem)
395
+ save_policies(d, path)
396
+ return {"ok": True, "subsystem": subsystem,
397
+ "subsystems": sorted(d.get("subsystems") or {})}
398
+
399
+
400
+ # --------------------------------------------------------------------------
401
+ # 策略驱动的签名 / 验签(连接层调用)
402
+ # --------------------------------------------------------------------------
403
+
404
+ # 生效条件:action 不在 policy_for(subsystem, path)["sign_on"] 中时返回 signed=False、signer=None、signature="";在列内时由 get_signer(pol.get("signer")) 对 payload 签名并返回 signed=True 与 s.public()。
405
+ def sign_for(subsystem: str, payload, ctx: dict = None,
406
+ action: str = "handshake", path: str = None) -> dict:
407
+ """按子系统策略签名。策略未把该 action 列入 `sign_on` 则**不签**。"""
408
+ pol = policy_for(subsystem, path)
409
+ if action not in pol["sign_on"]:
410
+ return {"ok": True, "signed": False, "action": action,
411
+ "signer": None, "signature": "",
412
+ "reason": f"子系统 {subsystem or '(默认)'} 策略未对 {action} 要求签名"}
413
+ s = get_signer(pol.get("signer"))
414
+ sig = s.sign(payload, ctx)
415
+ return {"ok": True, "signed": True, "action": action,
416
+ "signer": getattr(s, "name", None), "signature": sig,
417
+ "public": s.public()}
418
+
419
+
420
+ # 生效条件:required=bool(pol["require_peer_signature"]),signature 经 (signature or "").strip() 后为空且 required 为假时返回 ok=True/required=False,required 为真且为空时返回 ok=False/missing=True/on_fail="reject",签名非空时由 get_signer(pol.get("signer")) 验签、异常置 ok=False 且 err=str(e),失败时 on_fail 取 pol["on_verify_fail"]。
421
+ def verify_for(subsystem: str, payload, signature, ctx: dict = None,
422
+ action: str = "handshake", path: str = None) -> dict:
423
+ """按子系统策略验对端签名。
424
+
425
+ 语义分工(避免歧义):
426
+ · `sign_on` —— **本节点**在哪些动作上签名(见 `sign_for`);
427
+ · `require_peer_signature` —— **对端**是否必须提供签名。
428
+
429
+ 三种结果:
430
+ · 不要求且未提供 → `ok=True, required=False`(放行);
431
+ · 要求但缺失 → `ok=False, missing=True, on_fail="reject"`(fail-closed);
432
+ · 提供了就验 → 失败时 `on_fail` 取策略值,由调用方执行处置。
433
+ """
434
+ pol = policy_for(subsystem, path)
435
+ required = bool(pol["require_peer_signature"])
436
+ sig = (signature or "").strip()
437
+
438
+ if not required and not sig:
439
+ return {"ok": True, "required": False, "missing": False,
440
+ "action": action, "signer": None, "on_fail": None,
441
+ "require_peer_signature": False,
442
+ "reason": "策略未要求对端签名,且对端未提供"}
443
+ if required and not sig:
444
+ return {"ok": False, "required": True, "missing": True,
445
+ "action": action, "signer": None, "on_fail": "reject",
446
+ "require_peer_signature": True,
447
+ "reason": "策略要求对端签名,但对端未提供"}
448
+
449
+ s = get_signer(pol.get("signer"))
450
+ try:
451
+ ok = bool(s.verify(payload, sig, ctx))
452
+ except Exception as e:
453
+ ok, err = False, str(e)
454
+ else:
455
+ err = ""
456
+ return {"ok": ok, "required": required, "missing": False, "action": action,
457
+ "signer": getattr(s, "name", None),
458
+ "on_fail": None if ok else pol["on_verify_fail"],
459
+ "require_peer_signature": required,
460
+ "reason": "" if ok else (err or "验签失败")}
461
+
462
+
463
+ # --------------------------------------------------------------------------
464
+ # 自描述 / CLI
465
+ # --------------------------------------------------------------------------
466
+
467
+ # 生效条件:无必需形参,default_signer 取 d.get("default_signer") or DEFAULT_SIGNER,subsystems 缺失或为假值时按 {} 处理并逐个 _normalize,updated_at 取 d.get("updated_at")。
468
+ def show(path: str = None) -> dict:
469
+ d = load_policies(path)
470
+ return {"file": signers_file(path),
471
+ "default_signer": d.get("default_signer") or DEFAULT_SIGNER,
472
+ "subsystems": {k: _normalize(v) for k, v in
473
+ (d.get("subsystems") or {}).items()},
474
+ "updated_at": d.get("updated_at")}
475
+
476
+
477
+ # 生效条件:无必需形参,返回 SIGN_ACTIONS、ON_VERIFY_FAIL 的列表、list_signers()、_REGISTRY 中名为 "null" 或 DEFAULT_SIGNER 的内置签名器 describe(),以及 SIGNER_ENV/SIGNER_MODULE_ENV/SIGNERS_FILE_ENV/KEY_FILE_ENV 常量。
478
+ def catalog() -> dict:
479
+ return {
480
+ "layer": "签名接口(蜂群互联 D-4)",
481
+ "decision": "协议内核只定义契约;实现落工程项目;子系统自行决定用法",
482
+ "actions": list(SIGN_ACTIONS),
483
+ "on_verify_fail": list(ON_VERIFY_FAIL),
484
+ "signers": list_signers(),
485
+ "builtin": {n: _REGISTRY[n]().describe() for n in sorted(_REGISTRY)
486
+ if n in ("null", DEFAULT_SIGNER)},
487
+ "policy_file": signers_file(),
488
+ "env": {"signer": SIGNER_ENV, "module": SIGNER_MODULE_ENV,
489
+ "policy_file": SIGNERS_FILE_ENV, "key_file": KEY_FILE_ENV},
490
+ }
491
+
492
+
493
+ # 生效条件:obj 为任意对象时以 json.dumps(ensure_ascii=False, indent=1, default=str) 打印(不可序列化值经 default=str 转换),源码无返回值。
494
+ def _print(obj):
495
+ print(json.dumps(obj, ensure_ascii=False, indent=1, default=str))
496
+
497
+
498
+ # 生效条件:argv(默认取 sys.argv)经 argparse 解析出必填 cmd(catalog/list/show/set/rm/sign/verify 之一)后分派到对应分支;任一分支抛出 SignerError 时向 stderr 打印并返回 2,否则返回 0。
499
+ def main(argv=None):
500
+ ap = argparse.ArgumentParser(
501
+ prog="python -m md_cg.signer",
502
+ description="签名接口层:契约 + 子系统策略(D-4:内核不实现密码学)")
503
+ ap.add_argument("--signers-file", default=None,
504
+ help="策略文件(默认 ~/.mdcg/_signers.json)")
505
+ ap.add_argument("--key-file", default=None,
506
+ help="本地密钥文件(默认 ~/.mdcg/_signer.key)")
507
+ sub = ap.add_subparsers(dest="cmd", required=True)
508
+
509
+ sub.add_parser("catalog", help="签名接口自描述")
510
+ sub.add_parser("list", help="已注册签名器")
511
+ sub.add_parser("show", help="子系统签名策略")
512
+
513
+ p_set = sub.add_parser("set", help="声明子系统签名策略")
514
+ p_set.add_argument("subsystem")
515
+ p_set.add_argument("--signer", default=None)
516
+ p_set.add_argument("--sign-on", default=None, help="逗号分隔:handshake,evidence")
517
+ p_set.add_argument("--require-peer", action="store_true", default=None)
518
+ p_set.add_argument("--on-verify-fail", default=None,
519
+ choices=list(ON_VERIFY_FAIL))
520
+
521
+ p_rm = sub.add_parser("rm", help="删除子系统策略")
522
+ p_rm.add_argument("subsystem")
523
+
524
+ p_s = sub.add_parser("sign", help="按策略签名(payload 取自 --payload 或 stdin)")
525
+ p_s.add_argument("--subsystem", default=None)
526
+ p_s.add_argument("--action", default="handshake", choices=list(SIGN_ACTIONS))
527
+ p_s.add_argument("--payload", default=None)
528
+
529
+ p_v = sub.add_parser("verify", help="按策略验签")
530
+ p_v.add_argument("--subsystem", default=None)
531
+ p_v.add_argument("--action", default="handshake", choices=list(SIGN_ACTIONS))
532
+ p_v.add_argument("--payload", required=True)
533
+ p_v.add_argument("--signature", required=True)
534
+
535
+ a = ap.parse_args(argv)
536
+ try:
537
+ if a.cmd == "catalog":
538
+ _print(catalog())
539
+ elif a.cmd == "list":
540
+ _print(list_signers())
541
+ elif a.cmd == "show":
542
+ _print(show(a.signers_file))
543
+ elif a.cmd == "set":
544
+ on = ([x for x in a.sign_on.split(",") if x.strip()]
545
+ if a.sign_on else None)
546
+ _print(set_policy(a.subsystem, path=a.signers_file,
547
+ signer=a.signer, sign_on=on,
548
+ require_peer_signature=a.require_peer,
549
+ on_verify_fail=a.on_verify_fail))
550
+ elif a.cmd == "rm":
551
+ _print(remove_policy(a.subsystem, a.signers_file))
552
+ elif a.cmd == "sign":
553
+ payload = a.payload if a.payload is not None else sys.stdin.read()
554
+ _print(sign_for(a.subsystem, payload, action=a.action,
555
+ path=a.signers_file))
556
+ elif a.cmd == "verify":
557
+ _print(verify_for(a.subsystem, a.payload, a.signature,
558
+ action=a.action, path=a.signers_file))
559
+ except SignerError as e:
560
+ print(f"[md_cg.signer] {e}", file=sys.stderr)
561
+ return 2
562
+ return 0
563
+
564
+
565
+ if __name__ == "__main__":
563
566
  sys.exit(main())