hypermind 0.11.0__py3-none-any.whl

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 (181) hide show
  1. hmctl/__init__.py +7 -0
  2. hmctl/cli.py +947 -0
  3. hypermind/__init__.py +208 -0
  4. hypermind/_deprecations.py +108 -0
  5. hypermind/agent.py +2896 -0
  6. hypermind/agent_card.py +127 -0
  7. hypermind/api/__init__.py +32 -0
  8. hypermind/api/app.py +686 -0
  9. hypermind/capability.py +923 -0
  10. hypermind/consult.py +493 -0
  11. hypermind/consult_bias.py +112 -0
  12. hypermind/contract_net.py +219 -0
  13. hypermind/crypto/__init__.py +21 -0
  14. hypermind/crypto/cose_encrypt.py +126 -0
  15. hypermind/crypto/dkg.py +246 -0
  16. hypermind/crypto/domain.py +59 -0
  17. hypermind/crypto/frost.py +771 -0
  18. hypermind/crypto/hashing.py +38 -0
  19. hypermind/crypto/hlc.py +163 -0
  20. hypermind/crypto/hpke.py +277 -0
  21. hypermind/crypto/kms.py +196 -0
  22. hypermind/crypto/kms_backends/__init__.py +56 -0
  23. hypermind/crypto/kms_backends/aws.py +128 -0
  24. hypermind/crypto/kms_backends/azure.py +114 -0
  25. hypermind/crypto/kms_backends/gcp.py +97 -0
  26. hypermind/crypto/kms_backends/vault.py +120 -0
  27. hypermind/crypto/namespace_root.py +204 -0
  28. hypermind/crypto/pq.py +152 -0
  29. hypermind/crypto/room_epoch.py +97 -0
  30. hypermind/crypto/signing.py +245 -0
  31. hypermind/deliberation.py +355 -0
  32. hypermind/did_web.py +256 -0
  33. hypermind/dispute.py +858 -0
  34. hypermind/errors.py +218 -0
  35. hypermind/eval/__init__.py +49 -0
  36. hypermind/eval/calibration.py +307 -0
  37. hypermind/eval/comparison.py +251 -0
  38. hypermind/eval/replay.py +202 -0
  39. hypermind/eval/swarm_iq.py +718 -0
  40. hypermind/knowledge/__init__.py +17 -0
  41. hypermind/knowledge/citation_graph.py +141 -0
  42. hypermind/knowledge/correlated_agreement.py +167 -0
  43. hypermind/knowledge/embedding_registry.py +115 -0
  44. hypermind/knowledge/kernel_store.py +197 -0
  45. hypermind/knowledge/rekor.py +173 -0
  46. hypermind/knowledge/reputation.py +364 -0
  47. hypermind/knowledge/swarm_memory.py +523 -0
  48. hypermind/knowledge/transparency.py +409 -0
  49. hypermind/mcp/__init__.py +60 -0
  50. hypermind/mcp/bridge.py +151 -0
  51. hypermind/mcp/client.py +142 -0
  52. hypermind/mcp/provenance.py +214 -0
  53. hypermind/mcp/relata_tools.py +152 -0
  54. hypermind/mcp/server.py +248 -0
  55. hypermind/mcp/transport.py +206 -0
  56. hypermind/mind/__init__.py +150 -0
  57. hypermind/mind/active.py +234 -0
  58. hypermind/mind/belief.py +369 -0
  59. hypermind/mind/deliberator.py +625 -0
  60. hypermind/mind/diversity.py +144 -0
  61. hypermind/mind/evolve.py +184 -0
  62. hypermind/mind/federation.py +154 -0
  63. hypermind/mind/goals.py +117 -0
  64. hypermind/mind/integration.py +190 -0
  65. hypermind/mind/mediator.py +74 -0
  66. hypermind/mind/memory_store.py +129 -0
  67. hypermind/mind/policy.py +307 -0
  68. hypermind/mind/records.py +260 -0
  69. hypermind/mind/roles.py +190 -0
  70. hypermind/mind/sybil_guard.py +41 -0
  71. hypermind/mind/world_model.py +166 -0
  72. hypermind/namespace.py +515 -0
  73. hypermind/namespace_policy.py +254 -0
  74. hypermind/observability/__init__.py +25 -0
  75. hypermind/observability/asgi.py +214 -0
  76. hypermind/observability/cost.py +277 -0
  77. hypermind/observability/otel_export.py +200 -0
  78. hypermind/observability/recorder.py +344 -0
  79. hypermind/observability/swarm_trace.py +438 -0
  80. hypermind/pin_policy.py +116 -0
  81. hypermind/py.typed +0 -0
  82. hypermind/responders/__init__.py +87 -0
  83. hypermind/responders/anthropic.py +159 -0
  84. hypermind/responders/base.py +162 -0
  85. hypermind/responders/openai.py +199 -0
  86. hypermind/responders/router.py +254 -0
  87. hypermind/responders/structured.py +287 -0
  88. hypermind/responders/tools.py +444 -0
  89. hypermind/revocation.py +270 -0
  90. hypermind/rule_ids.py +135 -0
  91. hypermind/schemas/encrypted_statement.cddl +28 -0
  92. hypermind/schemas/namespace_accept.cddl +20 -0
  93. hypermind/schemas/namespace_create.cddl +25 -0
  94. hypermind/schemas/namespace_founding_attest.cddl +30 -0
  95. hypermind/schemas/namespace_invite.cddl +22 -0
  96. hypermind/schemas/wire-v0.1.cddl +73 -0
  97. hypermind/simlab/__init__.py +15 -0
  98. hypermind/simlab/_persona_registry.py +93 -0
  99. hypermind/simlab/_scenario_impl.py +2778 -0
  100. hypermind/simlab/app.py +2538 -0
  101. hypermind/simlab/backends/__init__.py +27 -0
  102. hypermind/simlab/backends/anchor.py +88 -0
  103. hypermind/simlab/backends/local.py +32 -0
  104. hypermind/simlab/backends/server.py +79 -0
  105. hypermind/simlab/cli_command.py +108 -0
  106. hypermind/simlab/config.py +106 -0
  107. hypermind/simlab/deployment.py +26 -0
  108. hypermind/simlab/knowledge.py +716 -0
  109. hypermind/simlab/learning.py +295 -0
  110. hypermind/simlab/modes/__init__.py +115 -0
  111. hypermind/simlab/modes/_eval_real_llm.py +373 -0
  112. hypermind/simlab/modes/aar.py +611 -0
  113. hypermind/simlab/modes/backtest.py +610 -0
  114. hypermind/simlab/modes/binary_forecast.py +69 -0
  115. hypermind/simlab/modes/conformance.py +1869 -0
  116. hypermind/simlab/modes/evaluation.py +2271 -0
  117. hypermind/simlab/modes/governance.py +648 -0
  118. hypermind/simlab/modes/redteam.py +534 -0
  119. hypermind/simlab/modes/tabletop.py +797 -0
  120. hypermind/simlab/modes/tournament.py +448 -0
  121. hypermind/simlab/modes/whatif.py +523 -0
  122. hypermind/simlab/namespace.py +125 -0
  123. hypermind/simlab/personas.py +477 -0
  124. hypermind/simlab/prompt_generator.py +172 -0
  125. hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
  126. hypermind/simlab/question_sets/governance_q5.json +67 -0
  127. hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
  128. hypermind/simlab/question_sets/whatif_q3.json +38 -0
  129. hypermind/simlab/registry.py +325 -0
  130. hypermind/simlab/runner.py +239 -0
  131. hypermind/simlab/scenario.py +147 -0
  132. hypermind/simlab/swarms.py +180 -0
  133. hypermind/simlab/tool_handlers/__init__.py +4 -0
  134. hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
  135. hypermind/simlab/tool_handlers/brave.py +46 -0
  136. hypermind/simlab/tool_handlers/newsapi.py +50 -0
  137. hypermind/simlab/tool_handlers/openweather.py +46 -0
  138. hypermind/simlab/tool_handlers/pinecone.py +52 -0
  139. hypermind/simlab/tool_handlers/qdrant.py +57 -0
  140. hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
  141. hypermind/simlab/tool_handlers/relata_recall.py +61 -0
  142. hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
  143. hypermind/simlab/tool_handlers/serper.py +46 -0
  144. hypermind/simlab/tool_handlers/tavily.py +53 -0
  145. hypermind/simlab/tool_handlers/weaviate.py +65 -0
  146. hypermind/simlab/tool_handlers/wikipedia.py +64 -0
  147. hypermind/simlab/tool_handlers/wolfram.py +48 -0
  148. hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
  149. hypermind/simlab/tool_registry.py +568 -0
  150. hypermind/simlab/topic_adapter.py +338 -0
  151. hypermind/simlab/topic_questions.py +259 -0
  152. hypermind/storage/__init__.py +69 -0
  153. hypermind/storage/backend.py +71 -0
  154. hypermind/storage/relata.py +245 -0
  155. hypermind/storage/sqlite.py +258 -0
  156. hypermind/sync.py +191 -0
  157. hypermind/tal.py +175 -0
  158. hypermind/tasks.py +334 -0
  159. hypermind/testing.py +16 -0
  160. hypermind/tools/__init__.py +32 -0
  161. hypermind/tools/registry.py +61 -0
  162. hypermind/transport/__init__.py +22 -0
  163. hypermind/transport/anti_entropy.py +708 -0
  164. hypermind/transport/bus.py +206 -0
  165. hypermind/transport/knows_delta.py +204 -0
  166. hypermind/transport/libp2p.py +298 -0
  167. hypermind/transport/placement.py +58 -0
  168. hypermind/transport/tcp.py +797 -0
  169. hypermind/transport/tls_profile.py +417 -0
  170. hypermind/types.py +59 -0
  171. hypermind/uncertainty.py +222 -0
  172. hypermind/wire.py +897 -0
  173. hypermind/workflow/__init__.py +72 -0
  174. hypermind/workflow/engine.py +655 -0
  175. hypermind/workflow/plan.py +182 -0
  176. hypermind/workflow/repair.py +203 -0
  177. hypermind-0.11.0.dist-info/METADATA +524 -0
  178. hypermind-0.11.0.dist-info/RECORD +181 -0
  179. hypermind-0.11.0.dist-info/WHEEL +4 -0
  180. hypermind-0.11.0.dist-info/entry_points.txt +2 -0
  181. hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
hypermind/wire.py ADDED
@@ -0,0 +1,897 @@
1
+ """COSE/CBOR envelope — Signed Statements, Receipts, Disputes.
2
+
3
+ Implements `draft-hypermind-scitt-contested-claims-profile-00` wire shape.
4
+ Encoding is RFC 8949 deterministic CBOR (§4.2). Every envelope
5
+ discriminator is bound into the signed Sig_structure preimage
6
+ (A-ALG-BOUND), preventing cross-version substitution attacks.
7
+
8
+ Protected header label allocations (proposed IANA -65600..-65624):
9
+ -65601 record_type
10
+ -65602 namespace_id (16-byte UUIDv7 of namespace policy root)
11
+ -65603 hlc ([logical_ms, counter, node_id])
12
+ -65604 sig_alg_disc (mirrors COSE alg=1; bound into preimage)
13
+ -65605 envelope_schema_hash (BLAKE3 of CDDL)
14
+ -65606 wire_version (= 0)
15
+ -65607 pq_required (bool)
16
+ -65608 receipt_uncertainty (RECEIPT only)
17
+ -65609 confirmer_set (SEAL only, ≥3)
18
+ -65610 dispute_state
19
+ -65611 protocol_version (= 0; values ≥1 reserved for future profiles)
20
+ -65620 dispute_target_kid
21
+ -65621 dispute_subtype
22
+ -65622 bond_commitment
23
+ -65623 prior_dispute_chain
24
+ -65624 ramp_state
25
+ -65625 valid_from_hlc (reserved, Belief State Layer §21)
26
+ -65626 valid_until_hlc (reserved, Belief State Layer §21)
27
+ -65627 mcp_provenance (T1-03; MCP tool-call provenance)
28
+ -65628..-65629 reserved for Saga Workflow record types (T2)
29
+ -65630 rfp_id (Contract-Net §27, T3)
30
+ -65631 bid_kid (Contract-Net §27, T3)
31
+ -65632 winning_bid_kid (Contract-Net §27, T3)
32
+ -65633 award_signer_kids (Contract-Net §27, T3)
33
+ -65634 bidder_filter (Contract-Net §27, T3)
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import importlib.resources
39
+ import secrets
40
+ from dataclasses import dataclass, field
41
+ from typing import Any, Literal
42
+
43
+ import cbor2
44
+
45
+ from hypermind.crypto.hashing import blake3_32, sha256_kid
46
+ from hypermind.crypto.hlc import HLC
47
+ from hypermind.crypto.signing import (
48
+ ALG_ED25519,
49
+ ALG_MLDSA65_ED25519,
50
+ ED25519_PK_LEN,
51
+ Keypair,
52
+ verify_signature,
53
+ )
54
+ from hypermind.errors import ConformanceError, ProtocolVersionError
55
+ from hypermind.uncertainty import Uncertainty
56
+
57
+ # Standard COSE labels
58
+ COSE_ALG = 1
59
+ COSE_KID = 4
60
+
61
+ # HyperMind extension labels
62
+ HM_RECORD_TYPE = -65601
63
+ HM_NAMESPACE_ID = -65602
64
+ HM_HLC = -65603
65
+ HM_SIG_ALG_DISC = -65604
66
+ HM_ENVELOPE_SCHEMA_HASH = -65605
67
+ HM_WIRE_VERSION = -65606
68
+ HM_PQ_REQUIRED = -65607
69
+ HM_RECEIPT_UNCERTAINTY = -65608
70
+ HM_CONFIRMER_SET = -65609
71
+ HM_DISPUTE_STATE = -65610
72
+ HM_PROTOCOL_VERSION = -65611
73
+
74
+ HM_DISPUTE_TARGET_KID = -65620
75
+ HM_DISPUTE_SUBTYPE = -65621
76
+ HM_BOND_COMMITMENT = -65622
77
+ HM_PRIOR_DISPUTE_CHAIN = -65623
78
+ HM_RAMP_STATE = -65624
79
+
80
+ # P3-05: Reserved for Belief State Layer §21 — not yet normative
81
+ HM_VALID_FROM_HLC = -65625 # reserved, Belief State Layer §21
82
+ HM_VALID_UNTIL_HLC = -65626 # reserved, Belief State Layer §21
83
+
84
+ # T1-03 (v0.10.1) — MCP tool-call provenance binding (spec §coordination-T1).
85
+ # Carries the triple (calling_agent_kid, cap_token_root, request_kid) inside a
86
+ # signed MCP JSON-RPC request envelope. The MCP server MUST reject any
87
+ # `tools/call` whose request lacks this header. The label is reserved here so
88
+ # every wire-label allocation lives in one place, even though MCP requests
89
+ # travel as JSON-RPC (not COSE_Sign1) on the wire — the same numeric label
90
+ # identifies the same semantic field on both transports.
91
+ HM_MCP_PROVENANCE = -65627 # reserved, MCP tool-call provenance (T1-03)
92
+
93
+ # T2 (Saga Workflow) reserves -65628 / -65629 — placeholder allocation so
94
+ # T3 can pick non-colliding numeric labels even though T2 has not landed
95
+ # yet. T2 is expected to add `WORKFLOW_STEP` and `WORKFLOW_COMPENSATE`
96
+ # record types plus saga-id / step-index protected-header fields.
97
+ HM_WORKFLOW_SAGA_ID = -65628 # reserved, Saga Workflow §coordination-T2
98
+ HM_WORKFLOW_STEP_INDEX = -65629 # reserved, Saga Workflow §coordination-T2
99
+
100
+ # T3 (Contract-Net Protocol §27) — RFP/Bid/Award/Dispute coordination.
101
+ # These five record types ride on top of the existing SignedStatement
102
+ # envelope; the new protected-header labels carry the tuple identifiers
103
+ # so a verifier can reject a forged bid/award without parsing the payload.
104
+ HM_RFP_ID = -65630 # Contract-Net rfp_id (32 bytes, sha256 of canonical RFP body)
105
+ HM_BID_KID = -65631 # Contract-Net bid_kid (32 bytes, kid of the bid SignedStatement)
106
+ HM_WINNING_BID_KID = -65632 # AWARD-only: kid of the winning bid
107
+ HM_AWARD_SIGNER_KIDS = -65633 # AWARD-only: list of FROST signer kids (≥1)
108
+ HM_BIDDER_FILTER = -65634 # RFP_OPEN-only: minimum-reputation floor + topic glob
109
+
110
+ # T6-01 (v0.10.4) — Distributed swarm-trace propagation. Carries a W3C
111
+ # TraceContext blob (16-byte trace_id || 8-byte span_id || 1-byte flags ||
112
+ # optional 8-byte parent_span_id) inside the protected header so every
113
+ # SignedStatement that emerges from a deliberation/workflow/RFP step can
114
+ # be stitched into a single OTel span tree across hosts. Reserved here so
115
+ # consumers that don't yet understand the label still preserve it round-
116
+ # trip through canonical re-encoding (A-ALG-BOUND).
117
+ HM_OTEL_TRACE_CONTEXT = -65635 # reserved, distributed swarm-trace (T6-01)
118
+
119
+ #: Highest wire_version this build knows how to mint and parse, and the
120
+ #: outbound default for newly-minted envelopes. Bumped to 2 in v0.9.x
121
+ #: Phase 2 to register the namespace-lifecycle record types
122
+ #: (NAMESPACE_CREATE / NAMESPACE_FOUNDING_ATTEST / NAMESPACE_INVITE /
123
+ #: NAMESPACE_ACCEPT) and the `EncryptedStatement` payload schema (§23).
124
+ WIRE_VERSION = 2
125
+
126
+ #: Alias for callers that branch on the highest known wire version.
127
+ WIRE_VERSION_LATEST = WIRE_VERSION
128
+
129
+ #: Maximum size (in bytes) of a wire-format envelope. Envelopes larger than
130
+ #: this are rejected at parse time before any CBOR decoding work. Bounds the
131
+ #: cost of malformed/oversized inputs and keeps gossip-bus memory usage
132
+ #: predictable. 65536 is comfortably above every legitimate signed statement
133
+ #: shape in v0.x (largest = composite-PQ DISPUTE with full prior chain ≈ 6 KiB).
134
+ MAX_ENVELOPE_BYTES = 65536
135
+
136
+ # Wire-version cutover support (panel A2-2 task #3).
137
+ #
138
+ # During an overlap window between v0 and v1 the agent accepts any envelope
139
+ # whose wire_version is in WIRE_VERSION_SUPPORTED but **prefers the newest**
140
+ # for outbound traffic (= max of the set, currently == WIRE_VERSION). A new
141
+ # version MUST be added to the supported set BEFORE flipping the default
142
+ # WIRE_VERSION constant — this guarantees a graceful overlap.
143
+ #
144
+ # Why v1 is listed BEFORE the v1 CDDL ships (review issue #2 — intentional)
145
+ # -----------------------------------------------------------------------
146
+ # This is a deliberate dual-stack design, not a leak in the version gate.
147
+ # Adding `1` to the supported set ahead of time is what makes the cutover
148
+ # non-breaking: deployed v0 peers must already be willing to parse a v1
149
+ # envelope BEFORE any peer starts emitting v1, otherwise the network
150
+ # fractures the moment the first v1 sender appears. The "supported set
151
+ # MUST be widened before flipping `WIRE_VERSION`" rule above is the
152
+ # normative form of this requirement (panel A2-2 task #3).
153
+ #
154
+ # Security argument for why dual-listing is safe right now:
155
+ # v1 currently shares the v0 envelope CDDL — `HM_ENVELOPE_SCHEMA_HASH` is
156
+ # the same value for both versions. An adversary forging `wire_version=1`
157
+ # on a payload that otherwise matches the v0 layout therefore changes
158
+ # nothing observable: every other discriminator (record_type, namespace_id,
159
+ # hlc, signature_alg, envelope_schema_hash, pq_required) is bound under
160
+ # A-ALG-BOUND, the COSE_Sign1 signature covers the full protected map,
161
+ # and `from_bytes` re-canonicalises the envelope and rejects on mismatch.
162
+ # The version byte by itself does not unlock any new parser branch.
163
+ #
164
+ # When v1 introduces a NEW envelope shape (different CDDL), a separate
165
+ # `HM_ENVELOPE_SCHEMA_HASH_V1` constant MUST be added and `from_bytes`
166
+ # updated to dispatch on `(wire_version, envelope_schema_hash)` jointly.
167
+ # The dual-stack acceptance test
168
+ # `tests/test_wire.py::TestDualStackWireVersion::test_v1_envelope_accepted_at_parse_time`
169
+ # pins today's "v1 round-trips through the v0 schema" property.
170
+ WIRE_VERSION_SUPPORTED: set[int] = {0, 1, 2}
171
+
172
+ #: New record type for the witness cosignatures that close a dispute under the
173
+ #: k-of-n quorum rule (panel A2-2 / `HM-DISPUTE-QUORUM-001`).
174
+ RECORD_DISPUTE_CLOSE_WITNESS = "DISPUTE_CLOSE_WITNESS"
175
+
176
+ #: HyperMind protocol-version negotiation primitive (v0.5; ROADMAP §0.5.1–0.5.3).
177
+ #: Distinct from `wire_version` — `wire_version` discriminates the COSE/CBOR
178
+ #: envelope shape; `protocol_version` discriminates the higher-level profile
179
+ #: rules (panel selection, dispute FSM transitions, reputation kernel formula,
180
+ #: etc.). v0.x ships with `protocol_version=0`. Values ≥1 are reserved for
181
+ #: future profiles negotiated via `min_protocol_version` namespace policy.
182
+ PROTOCOL_VERSION = 0
183
+
184
+ #: Set of protocol_version values accepted at parse time. Add new values
185
+ #: BEFORE flipping PROTOCOL_VERSION so existing peers can already parse
186
+ #: newer envelopes (same dual-stack rationale as WIRE_VERSION_SUPPORTED).
187
+ SUPPORTED_PROTOCOL_VERSIONS: set[int] = {0, 1}
188
+
189
+ RecordType = Literal[
190
+ "SEAL",
191
+ "RECEIPT",
192
+ "DISPUTE",
193
+ "KEY_ROTATE",
194
+ "NAMESPACE_CREATE",
195
+ "NAMESPACE_FOUNDING_ATTEST",
196
+ "NAMESPACE_INVITE",
197
+ "NAMESPACE_ACCEPT",
198
+ "DISPUTE_CLOSE_WITNESS",
199
+ # P3-04: reserved record types for Belief State Layer §21 (no handler yet)
200
+ "cite_query",
201
+ "cite_resp",
202
+ # T2 (v0.10.2): durable workflow record types — reserved for the
203
+ # workflow engine to publish step + compensation records as signed
204
+ # statements once the network-mode wire path lands. The in-process
205
+ # workflow engine in src/hypermind/workflow/ already records steps
206
+ # via transparency.append_and_cosign() using these labels for the
207
+ # leaf-tag domain; reserving them on the wire keeps T3's parallel
208
+ # RFP record-type expansion conflict-free.
209
+ "WORKFLOW_STEP",
210
+ "WORKFLOW_COMPENSATE",
211
+ # T3 (v0.10.2): Contract-Net Protocol §27 (FIPA00029 lineage).
212
+ # RFP_OPEN advertises a job, BIDs come back, AWARD picks a winner
213
+ # under a FROST k-of-n quorum signature when consortium-signed,
214
+ # BID_REJECT records non-winning outcomes (and is also published to
215
+ # revoke an awarded bid after a successful dispute), BID_WITHDRAW
216
+ # lets a bidder pull out before AWARD. Numeric protected-header
217
+ # labels for these records are -65630..-65634 (see top-of-module
218
+ # docstring); they sit AFTER T2's -65628/-65629 reservation.
219
+ "RFP_OPEN",
220
+ "BID",
221
+ "AWARD",
222
+ "BID_REJECT",
223
+ "BID_WITHDRAW",
224
+ ]
225
+
226
+ #: Record types that are gated on `wire_version >= 2`. v0/v1 envelopes
227
+ #: carrying these record_types are rejected at parse time.
228
+ V2_GATED_RECORD_TYPES: frozenset[str] = frozenset(
229
+ {
230
+ "NAMESPACE_CREATE",
231
+ "NAMESPACE_FOUNDING_ATTEST",
232
+ "NAMESPACE_INVITE",
233
+ "NAMESPACE_ACCEPT",
234
+ }
235
+ )
236
+ DisputeSubtype = Literal["OPEN", "ARGUE", "COUNTER", "FRIVOLOUS"]
237
+ DisputeState = Literal[
238
+ "OPEN", "ARGUE", "COUNTER", "FRIVOLOUS", "CLOSED", "UNRESOLVED_PARTITION", "MEDIATE"
239
+ ]
240
+
241
+
242
+ # A-ALG-BOUND: every discriminator that must be inside the signed
243
+ # preimage. The verifier rejects pre-signature-check if any of these
244
+ # is absent from the protected header.
245
+ DISCRIMINATOR_LABELS = (
246
+ COSE_ALG,
247
+ HM_RECORD_TYPE,
248
+ HM_NAMESPACE_ID,
249
+ HM_HLC,
250
+ HM_SIG_ALG_DISC,
251
+ HM_ENVELOPE_SCHEMA_HASH,
252
+ HM_WIRE_VERSION,
253
+ HM_PQ_REQUIRED,
254
+ HM_PROTOCOL_VERSION,
255
+ )
256
+
257
+
258
+ # Canonical CDDL schema fingerprint — BLAKE3 of the published CDDL file
259
+ # shipped in `hypermind/schemas/wire-v0.1.cddl`. Loaded once at import
260
+ # time. The independent reference verifier ships its own copy of the
261
+ # same CDDL bytes and must compute the same digest (enforced by
262
+ # `tests/test_schema_hash_pinned.py`).
263
+ # Read as bytes, then normalise CRLF → LF. Without normalisation, a Windows
264
+ # checkout (where git may rewrite LF to CRLF without a .gitattributes pin)
265
+ # would compute a different ENVELOPE_SCHEMA_HASH and reject every Appendix Z
266
+ # conformance vector. The schema fingerprint must be platform-invariant.
267
+ _CDDL_BYTES = (
268
+ (importlib.resources.files("hypermind") / "schemas" / "wire-v0.1.cddl")
269
+ .read_bytes()
270
+ .replace(b"\r\n", b"\n")
271
+ )
272
+ ENVELOPE_SCHEMA_HASH = blake3_32(_CDDL_BYTES)
273
+
274
+
275
+ def _load_cddl(name: str) -> bytes:
276
+ """Load and CRLF-normalise a packaged CDDL file."""
277
+ return (
278
+ (importlib.resources.files("hypermind") / "schemas" / name)
279
+ .read_bytes()
280
+ .replace(b"\r\n", b"\n")
281
+ )
282
+
283
+
284
+ # Per-record-type CDDL fingerprints registered for wire_version >= 2.
285
+ # For v2 record_types whose payload has its own CDDL, the BLAKE3-256
286
+ # digest of that payload schema is bound into the protected header
287
+ # under HM_ENVELOPE_SCHEMA_HASH. v0/v1 record_types continue to carry
288
+ # the envelope CDDL digest (`ENVELOPE_SCHEMA_HASH`) for backwards
289
+ # compatibility — the v2 payload CDDL files were added by Phase 2 and
290
+ # only apply to the new record types.
291
+ _NAMESPACE_CREATE_CDDL = _load_cddl("namespace_create.cddl")
292
+ _NAMESPACE_FOUNDING_ATTEST_CDDL = _load_cddl("namespace_founding_attest.cddl")
293
+ _NAMESPACE_INVITE_CDDL = _load_cddl("namespace_invite.cddl")
294
+ _NAMESPACE_ACCEPT_CDDL = _load_cddl("namespace_accept.cddl")
295
+ _ENCRYPTED_STATEMENT_CDDL = _load_cddl("encrypted_statement.cddl")
296
+
297
+ NAMESPACE_CREATE_SCHEMA_HASH = blake3_32(_NAMESPACE_CREATE_CDDL)
298
+ NAMESPACE_FOUNDING_ATTEST_SCHEMA_HASH = blake3_32(_NAMESPACE_FOUNDING_ATTEST_CDDL)
299
+ NAMESPACE_INVITE_SCHEMA_HASH = blake3_32(_NAMESPACE_INVITE_CDDL)
300
+ NAMESPACE_ACCEPT_SCHEMA_HASH = blake3_32(_NAMESPACE_ACCEPT_CDDL)
301
+ ENCRYPTED_STATEMENT_SCHEMA_HASH = blake3_32(_ENCRYPTED_STATEMENT_CDDL)
302
+
303
+ #: Mapping record_type -> expected envelope_schema_hash for v2-gated
304
+ #: records. Records not present here use the default envelope CDDL
305
+ #: digest (`ENVELOPE_SCHEMA_HASH`).
306
+ RECORD_TYPE_SCHEMA_HASHES: dict[str, bytes] = {
307
+ "NAMESPACE_CREATE": NAMESPACE_CREATE_SCHEMA_HASH,
308
+ "NAMESPACE_FOUNDING_ATTEST": NAMESPACE_FOUNDING_ATTEST_SCHEMA_HASH,
309
+ "NAMESPACE_INVITE": NAMESPACE_INVITE_SCHEMA_HASH,
310
+ "NAMESPACE_ACCEPT": NAMESPACE_ACCEPT_SCHEMA_HASH,
311
+ }
312
+
313
+
314
+ def expected_schema_hash(record_type: str) -> bytes:
315
+ """Return the BLAKE3-256 schema hash bound for a given record_type."""
316
+ return RECORD_TYPE_SCHEMA_HASHES.get(record_type, ENVELOPE_SCHEMA_HASH)
317
+
318
+
319
+ Kid = bytes # 32-byte content-addressed claim id
320
+
321
+
322
+ def _det_cbor(obj: Any) -> bytes:
323
+ """RFC 8949 §4.2 deterministic CBOR encode."""
324
+ return cbor2.dumps(obj, canonical=True)
325
+
326
+
327
+ def _count_raw_map_pairs(map_bytes: bytes) -> int:
328
+ """Count the number of (key, value) pairs declared in a top-level CBOR map.
329
+
330
+ Reads only the map header — does NOT walk the values. Used to detect
331
+ duplicate keys in the protected header: cbor2 silently merges duplicates
332
+ into a Python dict (last-wins), so comparing the *declared* pair count
333
+ against `len(decoded_dict)` reveals the attack. The major-type byte must
334
+ be 5 (map). Indefinite-length maps (0xbf) are rejected as non-canonical.
335
+ """
336
+ if not map_bytes:
337
+ raise ConformanceError("protected header is empty")
338
+ b0 = map_bytes[0]
339
+ major = b0 >> 5
340
+ if major != 5:
341
+ raise ConformanceError("protected header must be a CBOR map")
342
+ info = b0 & 0x1F
343
+ if info < 24:
344
+ return info
345
+ if info == 24:
346
+ return map_bytes[1]
347
+ if info == 25:
348
+ return int.from_bytes(map_bytes[1:3], "big")
349
+ if info == 26:
350
+ return int.from_bytes(map_bytes[1:5], "big")
351
+ if info == 27:
352
+ return int.from_bytes(map_bytes[1:9], "big")
353
+ # info == 31 -> indefinite length; non-canonical per RFC 8949 §4.2.
354
+ raise ConformanceError("indefinite-length map is not canonical CBOR")
355
+
356
+
357
+ def canonical_sig_input(protected_map: dict[int, Any], payload: bytes) -> bytes:
358
+ """Build the COSE_Sign1 Sig_structure preimage.
359
+
360
+ Per RFC 9052 §4.4. Includes context "Signature1", protected, ext_aad
361
+ (empty), and payload. The protected map is encoded as deterministic
362
+ CBOR before being included (so re-ordering keys cannot produce a
363
+ different signature).
364
+ """
365
+ protected_bytes = _det_cbor(protected_map)
366
+ sig_structure = ["Signature1", protected_bytes, b"", payload]
367
+ return _det_cbor(sig_structure)
368
+
369
+
370
+ def _check_a_alg_bound(protected_map: dict[int, Any]) -> None:
371
+ """A-ALG-BOUND: every discriminator MUST be in the protected map.
372
+
373
+ Reject pre-verify (not signature-error) if any is missing — this is
374
+ the JWT-`alg=none` / SAML-XML-signature-wrapping defence.
375
+ """
376
+ missing = [lbl for lbl in DISCRIMINATOR_LABELS if lbl not in protected_map]
377
+ if missing:
378
+ raise ConformanceError(
379
+ f"A-ALG-BOUND violation: missing discriminators {missing} from protected header",
380
+ telemetry={"event_type": "A_ALG_BOUND_VIOLATION", "missing": missing},
381
+ )
382
+
383
+ # Cross-check: COSE alg and sig_alg_disc must agree.
384
+ if protected_map[COSE_ALG] != protected_map[HM_SIG_ALG_DISC]:
385
+ raise ConformanceError(
386
+ f"A-ALG-BOUND violation: COSE alg={protected_map[COSE_ALG]} "
387
+ f"≠ sig_alg_disc={protected_map[HM_SIG_ALG_DISC]}",
388
+ telemetry={"event_type": "A_ALG_BOUND_VIOLATION_DISCRIMINATOR_MISMATCH"},
389
+ )
390
+
391
+
392
+ def new_namespace_id() -> bytes:
393
+ """Generate a fresh namespace id (16 random bytes; UUIDv7-shaped)."""
394
+ return secrets.token_bytes(16)
395
+
396
+
397
+ @dataclass
398
+ class SignedStatement:
399
+ """A HyperMind Signed Statement (COSE_Sign1 with HyperMind extensions).
400
+
401
+ Wire form is `[protected_bytes, unprotected, payload, signature]`.
402
+
403
+ `issuer_kid` is always the 32-byte stable agent address (Ed25519
404
+ public-key half). For composite signatures (alg=-49) the full public
405
+ key (Ed25519 || ML-DSA-65 = 1984 bytes) is carried in the
406
+ unprotected header under key "issuer_pk_full" so verifiers can
407
+ validate the ML-DSA half without needing a key lookup service.
408
+ """
409
+
410
+ record_type: RecordType
411
+ namespace_id: bytes
412
+ hlc: HLC
413
+ issuer_kid: bytes
414
+ payload: bytes
415
+ signature: bytes
416
+ alg: int = ALG_ED25519
417
+ pq_required: bool = False
418
+ protocol_version: int = PROTOCOL_VERSION
419
+ issuer_pk_full: bytes = b"" # populated for composite alg
420
+ confirmer_set: list[bytes] = field(default_factory=list)
421
+ receipt_uncertainty: Uncertainty | None = None
422
+ dispute_state: DisputeState | None = None
423
+ # dispute fields (only populated for record_type == "DISPUTE"):
424
+ dispute_target_kid: bytes | None = None
425
+ dispute_subtype: DisputeSubtype | None = None
426
+ bond_commitment: dict[str, Any] | None = None
427
+ prior_dispute_chain: list[bytes] = field(default_factory=list)
428
+ ramp_state: dict[str, Any] | None = None
429
+ unprotected: dict[Any, Any] = field(default_factory=dict)
430
+ #: Wire-version of this envelope. Defaults to the newest version this
431
+ #: build prefers for outbound traffic. During an overlap window (panel
432
+ #: A2-2 task #3) `from_bytes` may parse any version in
433
+ #: `WIRE_VERSION_SUPPORTED` and round-trips preserve the original.
434
+ wire_version: int = WIRE_VERSION
435
+
436
+ # ---- construction ----------------------------------------------------
437
+
438
+ @classmethod
439
+ def sign(
440
+ cls,
441
+ *,
442
+ keypair: Keypair,
443
+ record_type: RecordType,
444
+ namespace_id: bytes,
445
+ hlc: HLC,
446
+ payload: bytes,
447
+ pq_required: bool = False,
448
+ protocol_version: int = PROTOCOL_VERSION,
449
+ confirmer_set: list[bytes] | None = None,
450
+ receipt_uncertainty: Uncertainty | None = None,
451
+ dispute_state: DisputeState | None = None,
452
+ dispute_target_kid: bytes | None = None,
453
+ dispute_subtype: DisputeSubtype | None = None,
454
+ bond_commitment: dict[str, Any] | None = None,
455
+ prior_dispute_chain: list[bytes] | None = None,
456
+ ramp_state: dict[str, Any] | None = None,
457
+ wire_version: int | None = None,
458
+ ) -> SignedStatement:
459
+ if wire_version is None:
460
+ wire_version = WIRE_VERSION
461
+ protected = cls._build_protected_header(
462
+ alg=keypair.alg,
463
+ record_type=record_type,
464
+ namespace_id=namespace_id,
465
+ hlc=hlc,
466
+ issuer_kid=keypair.kid,
467
+ pq_required=pq_required,
468
+ protocol_version=protocol_version,
469
+ confirmer_set=confirmer_set,
470
+ receipt_uncertainty=receipt_uncertainty,
471
+ dispute_state=dispute_state,
472
+ dispute_target_kid=dispute_target_kid,
473
+ dispute_subtype=dispute_subtype,
474
+ bond_commitment=bond_commitment,
475
+ prior_dispute_chain=prior_dispute_chain,
476
+ ramp_state=ramp_state,
477
+ wire_version=wire_version,
478
+ )
479
+ sig_input = canonical_sig_input(protected, payload)
480
+ signature = keypair.sign(sig_input)
481
+ issuer_pk_full = keypair.public_bytes if keypair.alg == ALG_MLDSA65_ED25519 else b""
482
+ return cls(
483
+ record_type=record_type,
484
+ namespace_id=namespace_id,
485
+ hlc=hlc,
486
+ issuer_kid=keypair.kid,
487
+ payload=payload,
488
+ signature=signature,
489
+ alg=keypair.alg,
490
+ pq_required=pq_required,
491
+ protocol_version=protocol_version,
492
+ issuer_pk_full=issuer_pk_full,
493
+ confirmer_set=list(confirmer_set or []),
494
+ receipt_uncertainty=receipt_uncertainty,
495
+ dispute_state=dispute_state,
496
+ dispute_target_kid=dispute_target_kid,
497
+ dispute_subtype=dispute_subtype,
498
+ bond_commitment=bond_commitment,
499
+ prior_dispute_chain=list(prior_dispute_chain or []),
500
+ ramp_state=ramp_state,
501
+ wire_version=wire_version,
502
+ )
503
+
504
+ # ---- header construction --------------------------------------------
505
+
506
+ @staticmethod
507
+ def _build_protected_header(
508
+ *,
509
+ alg: int,
510
+ record_type: RecordType,
511
+ namespace_id: bytes,
512
+ hlc: HLC,
513
+ issuer_kid: bytes,
514
+ pq_required: bool,
515
+ protocol_version: int,
516
+ confirmer_set: list[bytes] | None,
517
+ receipt_uncertainty: Uncertainty | None,
518
+ dispute_state: DisputeState | None,
519
+ dispute_target_kid: bytes | None,
520
+ dispute_subtype: DisputeSubtype | None,
521
+ bond_commitment: dict[str, Any] | None,
522
+ prior_dispute_chain: list[bytes] | None,
523
+ ramp_state: dict[str, Any] | None,
524
+ wire_version: int = WIRE_VERSION,
525
+ ) -> dict[int, Any]:
526
+ if len(namespace_id) != 16:
527
+ raise ValueError("namespace_id must be 16 bytes")
528
+ if record_type in V2_GATED_RECORD_TYPES and wire_version < 2:
529
+ raise ValueError(
530
+ f"record_type {record_type!r} requires wire_version >= 2 (got {wire_version})"
531
+ )
532
+ protected: dict[int, Any] = {
533
+ COSE_ALG: alg,
534
+ COSE_KID: issuer_kid,
535
+ HM_RECORD_TYPE: record_type,
536
+ HM_NAMESPACE_ID: namespace_id,
537
+ HM_HLC: hlc.to_cbor(),
538
+ HM_SIG_ALG_DISC: alg,
539
+ HM_ENVELOPE_SCHEMA_HASH: expected_schema_hash(record_type),
540
+ HM_WIRE_VERSION: wire_version,
541
+ HM_PQ_REQUIRED: pq_required,
542
+ HM_PROTOCOL_VERSION: protocol_version,
543
+ }
544
+ if receipt_uncertainty is not None:
545
+ if record_type != "RECEIPT":
546
+ raise ValueError("receipt_uncertainty only valid on RECEIPT")
547
+ protected[HM_RECEIPT_UNCERTAINTY] = receipt_uncertainty.to_cbor()
548
+ if confirmer_set:
549
+ if record_type != "SEAL":
550
+ raise ValueError("confirmer_set only valid on SEAL")
551
+ if len(confirmer_set) < 3:
552
+ raise ValueError("confirmer_set must have ≥3 entries")
553
+ protected[HM_CONFIRMER_SET] = list(confirmer_set)
554
+ if dispute_state is not None:
555
+ protected[HM_DISPUTE_STATE] = dispute_state
556
+ if record_type == "DISPUTE":
557
+ if dispute_target_kid is None or dispute_subtype is None or bond_commitment is None:
558
+ raise ValueError(
559
+ "DISPUTE record requires dispute_target_kid, "
560
+ "dispute_subtype, and bond_commitment"
561
+ )
562
+ if len(dispute_target_kid) != 32:
563
+ raise ValueError("dispute_target_kid must be 32 bytes")
564
+ protected[HM_DISPUTE_TARGET_KID] = dispute_target_kid
565
+ protected[HM_DISPUTE_SUBTYPE] = dispute_subtype
566
+ protected[HM_BOND_COMMITMENT] = bond_commitment
567
+ if prior_dispute_chain:
568
+ protected[HM_PRIOR_DISPUTE_CHAIN] = list(prior_dispute_chain)
569
+ if ramp_state:
570
+ protected[HM_RAMP_STATE] = ramp_state
571
+ return protected
572
+
573
+ # ---- accessors ------------------------------------------------------
574
+
575
+ @property
576
+ def _protected_bytes(self) -> bytes:
577
+ """Canonical deterministic CBOR of the protected header only.
578
+
579
+ Used as the sole input to ``kid`` — the unprotected header is
580
+ mutable and NOT signature-bound, so including it in the kid would
581
+ let an attacker change the kid without invalidating the signature
582
+ (P1-02 / HM-KID-PROTECTED-ONLY).
583
+ """
584
+ return _det_cbor(self.protected_map())
585
+
586
+ @property
587
+ def kid(self) -> Kid:
588
+ """Content-addressed kid: sha256(protected_header_bytes only).
589
+
590
+ Derived from the *protected* header exclusively so that mutations
591
+ to the unprotected header (which are NOT signature-bound) cannot
592
+ alter the kid without invalidating the signature. This closes the
593
+ P1-02 mutable-unprotected-header substitution class.
594
+ """
595
+ return sha256_kid(self._protected_bytes)
596
+
597
+ def protected_map(self) -> dict[int, Any]:
598
+ return self._build_protected_header(
599
+ alg=self.alg,
600
+ record_type=self.record_type,
601
+ namespace_id=self.namespace_id,
602
+ hlc=self.hlc,
603
+ issuer_kid=self.issuer_kid,
604
+ pq_required=self.pq_required,
605
+ protocol_version=self.protocol_version,
606
+ confirmer_set=self.confirmer_set,
607
+ receipt_uncertainty=self.receipt_uncertainty,
608
+ dispute_state=self.dispute_state,
609
+ dispute_target_kid=self.dispute_target_kid,
610
+ dispute_subtype=self.dispute_subtype,
611
+ bond_commitment=self.bond_commitment,
612
+ prior_dispute_chain=self.prior_dispute_chain,
613
+ ramp_state=self.ramp_state,
614
+ wire_version=self.wire_version,
615
+ )
616
+
617
+ # ---- serialization ---------------------------------------------------
618
+
619
+ def to_bytes(self) -> bytes:
620
+ """Encode as COSE_Sign1: [protected_bytes, unprotected, payload, signature].
621
+
622
+ For composite (alg=-49) the full issuer public key is carried in
623
+ the unprotected header so verifiers can validate without a key
624
+ lookup. The Ed25519 half (=kid) is also in the protected header
625
+ (label 4), preventing identity-substitution.
626
+ """
627
+ protected_bytes = _det_cbor(self.protected_map())
628
+ unprotected = dict(self.unprotected)
629
+ if self.issuer_pk_full and self.alg == ALG_MLDSA65_ED25519:
630
+ unprotected["issuer_pk_full"] = self.issuer_pk_full
631
+ return _det_cbor([protected_bytes, unprotected, self.payload, self.signature])
632
+
633
+ @classmethod
634
+ def from_bytes(cls, raw: bytes) -> SignedStatement:
635
+ if len(raw) > MAX_ENVELOPE_BYTES:
636
+ raise ConformanceError(
637
+ f"envelope exceeds maximum size: {len(raw)} > {MAX_ENVELOPE_BYTES} bytes"
638
+ )
639
+ try:
640
+ msg = cbor2.loads(raw)
641
+ except cbor2.CBORDecodeError as e:
642
+ raise ConformanceError(f"envelope is not valid CBOR: {e}") from e
643
+ if not isinstance(msg, list) or len(msg) != 4:
644
+ raise ConformanceError("envelope must be 4-element COSE_Sign1 array")
645
+ protected_bytes, unprotected, payload, signature = msg
646
+ try:
647
+ protected = cbor2.loads(protected_bytes)
648
+ except cbor2.CBORDecodeError as e:
649
+ raise ConformanceError(f"protected header is not valid CBOR: {e}") from e
650
+ if not isinstance(protected, dict):
651
+ raise ConformanceError("protected header must be a CBOR map")
652
+
653
+ # Duplicate-key gate: cbor2 silently merges duplicate map keys (last
654
+ # wins). Compare the *declared* pair count from the raw map header
655
+ # against the decoded dict length to surface the attack.
656
+ declared_pairs = _count_raw_map_pairs(protected_bytes)
657
+ if declared_pairs != len(protected):
658
+ raise ConformanceError("duplicate keys in protected header")
659
+
660
+ # Canonical-encoding gate (closes self-heal Cycle-1 Finding #7):
661
+ # re-encode the parsed protected map under deterministic CBOR
662
+ # and require byte-equality with the wire bytes. This prevents
663
+ # malleable encodings producing two distinct kids for the same
664
+ # logical statement.
665
+ if _det_cbor(protected) != protected_bytes:
666
+ raise ConformanceError("non-canonical protected header encoding (RFC 8949 §4.2)")
667
+
668
+ # Domain bound check happens here, BEFORE signature verify.
669
+ _check_a_alg_bound(protected)
670
+
671
+ # Wire-version + schema-hash gate: reject incompatible envelopes
672
+ # at parse time. A tampered wire_version (Z.7.4-class attack) is
673
+ # rejected here, not at signature-verify, so the failure mode is
674
+ # auditable (ConformanceError, not silent signature failure).
675
+ wv = protected[HM_WIRE_VERSION]
676
+ if wv not in WIRE_VERSION_SUPPORTED:
677
+ raise ConformanceError(
678
+ f"wire_version mismatch: got {wv}, expected one of {sorted(WIRE_VERSION_SUPPORTED)}"
679
+ )
680
+ record_type_for_hash = protected[HM_RECORD_TYPE]
681
+ if record_type_for_hash in V2_GATED_RECORD_TYPES and wv < 2:
682
+ raise ConformanceError(
683
+ f"record_type {record_type_for_hash!r} requires wire_version >= 2 (got {wv})"
684
+ )
685
+ expected_hash = expected_schema_hash(record_type_for_hash)
686
+ if protected[HM_ENVELOPE_SCHEMA_HASH] != expected_hash:
687
+ raise ConformanceError("envelope_schema_hash mismatch")
688
+
689
+ # P2-07: protocol_version dispatch — reject unsupported versions
690
+ # before signature verification so the refusal is auditable as a
691
+ # policy decision, not a crypto failure.
692
+ pv = int(protected[HM_PROTOCOL_VERSION])
693
+ if pv not in SUPPORTED_PROTOCOL_VERSIONS:
694
+ raise ProtocolVersionError(
695
+ f"Unsupported protocol version: {pv}; supported={sorted(SUPPORTED_PROTOCOL_VERSIONS)}",
696
+ statement_version=pv,
697
+ min_required=min(SUPPORTED_PROTOCOL_VERSIONS),
698
+ )
699
+
700
+ record_type = protected[HM_RECORD_TYPE]
701
+ unprotected_dict = dict(unprotected) if unprotected else {}
702
+ issuer_pk_full = bytes(unprotected_dict.pop("issuer_pk_full", b""))
703
+ return cls(
704
+ record_type=record_type,
705
+ namespace_id=bytes(protected[HM_NAMESPACE_ID]),
706
+ hlc=HLC.from_cbor(protected[HM_HLC]),
707
+ issuer_kid=bytes(protected[COSE_KID]),
708
+ payload=bytes(payload),
709
+ signature=bytes(signature),
710
+ alg=int(protected[COSE_ALG]),
711
+ pq_required=bool(protected[HM_PQ_REQUIRED]),
712
+ protocol_version=int(protected[HM_PROTOCOL_VERSION]),
713
+ issuer_pk_full=issuer_pk_full,
714
+ confirmer_set=[bytes(c) for c in protected.get(HM_CONFIRMER_SET, [])],
715
+ receipt_uncertainty=(
716
+ Uncertainty.from_cbor(protected[HM_RECEIPT_UNCERTAINTY])
717
+ if HM_RECEIPT_UNCERTAINTY in protected
718
+ else None
719
+ ),
720
+ dispute_state=protected.get(HM_DISPUTE_STATE),
721
+ dispute_target_kid=(
722
+ bytes(protected[HM_DISPUTE_TARGET_KID])
723
+ if HM_DISPUTE_TARGET_KID in protected
724
+ else None
725
+ ),
726
+ dispute_subtype=protected.get(HM_DISPUTE_SUBTYPE),
727
+ bond_commitment=protected.get(HM_BOND_COMMITMENT),
728
+ prior_dispute_chain=[bytes(k) for k in protected.get(HM_PRIOR_DISPUTE_CHAIN, [])],
729
+ ramp_state=protected.get(HM_RAMP_STATE),
730
+ unprotected=unprotected_dict,
731
+ wire_version=int(wv),
732
+ )
733
+
734
+ # ---- verification ----------------------------------------------------
735
+
736
+ def verify(self, *, revocation_store: Any = None) -> bool:
737
+ """Verify signature and A-ALG-BOUND.
738
+
739
+ Returns True if (a) every discriminator is in the signed
740
+ preimage, (b) wire_version + envelope_schema_hash match the
741
+ local v0 contract, (c) the signature is valid Ed25519 over the
742
+ canonical sig_input, and (d) — when a `revocation_store` is
743
+ supplied — the issuer kid is not revoked as of this statement's
744
+ HLC. Statements signed *before* a revocation remain valid; this
745
+ is the historical-claim-immutability property of v0.7's CRL
746
+ primitive (rule_id ``HM-KEY-REVOKED-001``).
747
+
748
+ Raises ConformanceError on A-ALG-BOUND or schema mismatch.
749
+ Returns False on signature failure or revocation hit (no
750
+ exception — callers can branch on the return value).
751
+ """
752
+ protected = self.protected_map()
753
+ _check_a_alg_bound(protected)
754
+
755
+ # Revocation gate: rejected BEFORE signature verify so a revoked
756
+ # kid cannot waste verifier CPU on signature math.
757
+ if revocation_store is not None:
758
+ entry = revocation_store.is_revoked(
759
+ bytes(self.issuer_kid),
760
+ (self.hlc.logical_ms, self.hlc.counter),
761
+ )
762
+ if entry is not None:
763
+ return False
764
+
765
+ if protected[HM_WIRE_VERSION] not in WIRE_VERSION_SUPPORTED:
766
+ raise ConformanceError(
767
+ f"wire_version mismatch: got {protected[HM_WIRE_VERSION]}, "
768
+ f"expected one of {sorted(WIRE_VERSION_SUPPORTED)}"
769
+ )
770
+ if protected[HM_ENVELOPE_SCHEMA_HASH] != expected_schema_hash(self.record_type):
771
+ raise ConformanceError("envelope_schema_hash mismatch")
772
+
773
+ if self.alg not in (ALG_ED25519, ALG_MLDSA65_ED25519):
774
+ raise ConformanceError(f"unsupported alg {self.alg}")
775
+
776
+ sig_input = canonical_sig_input(protected, self.payload)
777
+ if self.alg == ALG_ED25519:
778
+ return verify_signature(self.alg, self.issuer_kid, self.signature, sig_input)
779
+ # composite — need the full public key
780
+ full_pk = self.issuer_pk_full if len(self.issuer_pk_full) > 0 else self.issuer_kid
781
+ if len(full_pk) != ED25519_PK_LEN + 1952:
782
+ return False
783
+ # Bind the kid (Ed25519 half) to the full key — defeats identity
784
+ # substitution at parse time.
785
+ if full_pk[:ED25519_PK_LEN] != self.issuer_kid:
786
+ raise ConformanceError("issuer_kid does not match Ed25519 prefix of issuer_pk_full")
787
+ return verify_signature(self.alg, full_pk, self.signature, sig_input)
788
+
789
+ def __repr__(self) -> str:
790
+ return (
791
+ f"SignedStatement({self.record_type} kid={self.kid.hex()[:12]} "
792
+ f"hlc={self.hlc} ns={self.namespace_id.hex()[:8]})"
793
+ )
794
+
795
+
796
+ @dataclass
797
+ class EncryptedStatementRecipient:
798
+ """One recipient slot in an `EncryptedStatement` (§23.5)."""
799
+
800
+ recipient_kid: bytes # 32-byte Ed25519 pubkey half
801
+ enc: bytes # HPKE encapsulated key
802
+
803
+ def to_cbor(self) -> dict[str, bytes]:
804
+ if len(self.recipient_kid) != 32:
805
+ raise ValueError("recipient_kid must be 32 bytes")
806
+ return {"recipient_kid": self.recipient_kid, "enc": self.enc}
807
+
808
+ @classmethod
809
+ def from_cbor(cls, obj: Any) -> EncryptedStatementRecipient:
810
+ if not isinstance(obj, dict):
811
+ raise ConformanceError("Recipient must be a CBOR map")
812
+ if set(obj.keys()) != {"recipient_kid", "enc"}:
813
+ raise ConformanceError(
814
+ f"Recipient must have exactly keys recipient_kid+enc; got {sorted(obj.keys())}"
815
+ )
816
+ kid = bytes(obj["recipient_kid"])
817
+ enc = bytes(obj["enc"])
818
+ if len(kid) != 32:
819
+ raise ConformanceError("recipient_kid must be 32 bytes")
820
+ return cls(recipient_kid=kid, enc=enc)
821
+
822
+
823
+ @dataclass
824
+ class EncryptedStatement:
825
+ """HPKE-AEAD ciphertext payload for hybrid/private namespace records.
826
+
827
+ Wire form (RFC 8949 deterministic CBOR; see
828
+ `hypermind/schemas/encrypted_statement.cddl`):
829
+
830
+ [kdf, aead, [+ Recipient], aad_hash, ciphertext, ?epoch_tag]
831
+
832
+ `from_bytes` enforces canonical-CBOR (re-encode equality), array
833
+ arity, and 32-byte sizing of `aad_hash` / `recipient_kid` / optional
834
+ `epoch_tag` (16 bytes). The signature on the outer `SignedStatement`
835
+ covers `aad_hash` only — verification MUST NOT require decryption
836
+ (spec §23.3).
837
+ """
838
+
839
+ kdf: int
840
+ aead: int
841
+ enc_recipients: list[EncryptedStatementRecipient]
842
+ aad_hash: bytes # 32 bytes
843
+ ciphertext: bytes
844
+ epoch_tag: bytes | None = None # 16 bytes if present
845
+
846
+ def to_bytes(self) -> bytes:
847
+ if len(self.aad_hash) != 32:
848
+ raise ValueError("aad_hash must be 32 bytes")
849
+ if not self.enc_recipients:
850
+ raise ValueError("enc_recipients must contain at least one Recipient")
851
+ if self.epoch_tag is not None and len(self.epoch_tag) != 16:
852
+ raise ValueError("epoch_tag must be 16 bytes when present")
853
+ arr: list[Any] = [
854
+ int(self.kdf),
855
+ int(self.aead),
856
+ [r.to_cbor() for r in self.enc_recipients],
857
+ self.aad_hash,
858
+ self.ciphertext,
859
+ ]
860
+ if self.epoch_tag is not None:
861
+ arr.append(self.epoch_tag)
862
+ return _det_cbor(arr)
863
+
864
+ @classmethod
865
+ def from_bytes(cls, raw: bytes) -> EncryptedStatement:
866
+ if len(raw) > MAX_ENVELOPE_BYTES:
867
+ raise ConformanceError(
868
+ f"EncryptedStatement exceeds maximum size: {len(raw)} > {MAX_ENVELOPE_BYTES}"
869
+ )
870
+ try:
871
+ arr = cbor2.loads(raw)
872
+ except cbor2.CBORDecodeError as e:
873
+ raise ConformanceError(f"EncryptedStatement is not valid CBOR: {e}") from e
874
+ if not isinstance(arr, list) or len(arr) not in (5, 6):
875
+ raise ConformanceError("EncryptedStatement must be a 5- or 6-element CBOR array")
876
+ # Canonical-encoding gate (mirror SignedStatement.from_bytes).
877
+ if _det_cbor(arr) != raw:
878
+ raise ConformanceError("non-canonical EncryptedStatement encoding (RFC 8949 §4.2)")
879
+ kdf, aead, recipients_raw, aad_hash, ciphertext = arr[:5]
880
+ epoch_tag: bytes | None = bytes(arr[5]) if len(arr) == 6 else None
881
+ if not isinstance(kdf, int) or not isinstance(aead, int):
882
+ raise ConformanceError("kdf and aead must be integers")
883
+ if not isinstance(recipients_raw, list) or not recipients_raw:
884
+ raise ConformanceError("enc_recipients must be a non-empty array")
885
+ aad_hash_b = bytes(aad_hash)
886
+ if len(aad_hash_b) != 32:
887
+ raise ConformanceError("aad_hash must be 32 bytes")
888
+ if epoch_tag is not None and len(epoch_tag) != 16:
889
+ raise ConformanceError("epoch_tag must be 16 bytes when present")
890
+ return cls(
891
+ kdf=kdf,
892
+ aead=aead,
893
+ enc_recipients=[EncryptedStatementRecipient.from_cbor(r) for r in recipients_raw],
894
+ aad_hash=aad_hash_b,
895
+ ciphertext=bytes(ciphertext),
896
+ epoch_tag=epoch_tag,
897
+ )