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
@@ -0,0 +1,219 @@
1
+ """Contract-Net Protocol — RFP / Bid / Award / Dispute (spec §27, T3).
2
+
3
+ Lineage: FIPA Contract Net Protocol (FIPA00029). HyperMind's profile
4
+ adds three things FIPA does not specify:
5
+
6
+ 1. **Cryptographic settlement.** Every RFP, BID, and AWARD is a signed
7
+ :class:`hypermind.wire.SignedStatement` — no record of the auction
8
+ exists outside the SCITT log.
9
+ 2. **FROST quorum awards.** When a consortium of k-of-n principals must
10
+ co-sign an AWARD, the AWARD record is signed by a FROST aggregate
11
+ over the canonical preimage `H("hm:cnp-award:" || rfp_id || winning_bid_kid)`.
12
+ 3. **Disputable awards.** A losing bidder can dispute the AWARD via the
13
+ regular Dispute FSM; the bond's `topic_scope_hash` binds the dispute
14
+ to ``"rfp:" + rfp_id`` so a wash-trade across topics cannot slash
15
+ the bond.
16
+
17
+ This module owns the **scoring** layer. The agent verbs that publish the
18
+ signed records live on :class:`hypermind.agent.HyperMindAgent`.
19
+
20
+ Scoring is pluggable via the :class:`BidScorer` Protocol so federations
21
+ can swap in domain-specific scoring policies (e.g. resource-cost
22
+ weighted, latency-weighted) without touching the protocol itself. The
23
+ default :class:`ReputationWeightedScorer` combines the bidder's Beta
24
+ posterior on the topic with the bidder's claimed self-score and a
25
+ Brier-derived calibration term — so a high-reputation bidder beats a
26
+ low-reputation bidder at equal self-score.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import hashlib
32
+ from dataclasses import dataclass, field
33
+ from typing import Any, Protocol
34
+
35
+ from hypermind.knowledge.reputation import ReputationKernel
36
+
37
+ # Domain tag for the FROST award transcript context. v1: length-prefixed
38
+ # encoding (mirrors the witness-quorum tag `:v2` upgrade in dispute.py).
39
+ AWARD_DOMAIN_TAG = b"hypermind:cnp-award:v1"
40
+
41
+
42
+ def award_preimage(rfp_id: bytes, winning_bid_kid: bytes) -> bytes:
43
+ """Canonical preimage every AWARD signer (FROST or single) signs.
44
+
45
+ Domain-separated under :data:`AWARD_DOMAIN_TAG` so an AWARD signature
46
+ cannot be replayed as any other HyperMind signed payload. Both inputs
47
+ are length-prefixed with a 4-byte big-endian length to defeat boundary
48
+ ambiguity (mirrors the v2 witness-cosign encoding).
49
+ """
50
+
51
+ def _f(b: bytes) -> bytes:
52
+ return len(b).to_bytes(4, "big") + b
53
+
54
+ h = hashlib.sha256()
55
+ h.update(_f(AWARD_DOMAIN_TAG))
56
+ h.update(_f(bytes(rfp_id)))
57
+ h.update(_f(bytes(winning_bid_kid)))
58
+ return h.digest()
59
+
60
+
61
+ def topic_scope_for_rfp(rfp_id: bytes) -> str:
62
+ """Bond topic-scope string for disputes filed against an AWARD.
63
+
64
+ The dispute FSM hashes this string with `_hash_topic_scope` (BLAKE3)
65
+ when sealing the bond. The string itself is the stable identifier we
66
+ use everywhere AWARD disputes are negotiated — so the topic-scope
67
+ binding is `"rfp:" + hex(rfp_id)`.
68
+ """
69
+ return "rfp:" + bytes(rfp_id).hex()
70
+
71
+
72
+ # -----------------------------------------------------------------------
73
+ # Records — in-memory views of the signed RFP/BID/AWARD payloads
74
+ # -----------------------------------------------------------------------
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class RFP:
79
+ """In-memory view of an RFP_OPEN record's payload + identifiers."""
80
+
81
+ rfp_id: bytes
82
+ issuer_kid: bytes
83
+ topic: str
84
+ requirements: dict[str, Any]
85
+ deadline_hlc_ms: int
86
+ bidder_filter: dict[str, Any] | None = None
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class Bid:
91
+ """In-memory view of a BID record's payload + identifiers."""
92
+
93
+ rfp_id: bytes
94
+ bid_kid: bytes
95
+ bidder_kid: bytes
96
+ payload: dict[str, Any]
97
+ bid_score_self: float
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class Award:
102
+ """In-memory view of an AWARD record's payload + identifiers."""
103
+
104
+ rfp_id: bytes
105
+ award_kid: bytes
106
+ winning_bid_kid: bytes
107
+ winning_bidder_kid: bytes
108
+ award_signer_kids: list[bytes] = field(default_factory=list)
109
+ revoked: bool = False
110
+
111
+
112
+ # -----------------------------------------------------------------------
113
+ # Scoring
114
+ # -----------------------------------------------------------------------
115
+
116
+
117
+ class BidScorer(Protocol):
118
+ """Pluggable bid-scoring policy.
119
+
120
+ Implementations MUST be pure functions of (bid, requirements,
121
+ reputation_kernel). The `award()` agent verb invokes the configured
122
+ scorer for every received bid that passed the bidder_filter, then
123
+ awards the highest-scoring one.
124
+ """
125
+
126
+ def score(
127
+ self,
128
+ bid: Bid,
129
+ requirements: dict[str, Any],
130
+ reputation_kernel: ReputationKernel,
131
+ topic: str,
132
+ ) -> float:
133
+ """Return a real-valued score; higher wins ties resolved by bid_kid."""
134
+ ...
135
+
136
+
137
+ class ReputationWeightedScorer:
138
+ """Default scorer: rep-weighted self-score with calibration shrinkage.
139
+
140
+ Score formula (∈ [0, 2.5] in practice):
141
+
142
+ score = (rep_score * calibration) * bid_score_self
143
+ + 0.05 * rep_score # tie-breaker for equal self-score
144
+
145
+ where:
146
+ - ``rep_score`` is the Beta posterior mean for (bidder, topic) in
147
+ the supplied :class:`ReputationKernel`.
148
+ - ``calibration`` is the Brier-derived multiplicative shrinkage
149
+ in [0.5, 2.0] (same `calibration_adjustment` used by `consult()`),
150
+ defaulting to 1.0 when the agent has no oracle resolutions yet.
151
+ - ``bid_score_self`` is the bidder's claimed posterior on
152
+ delivering the requirement, supplied by the caller of `bid()`.
153
+
154
+ The `+ 0.05 * rep_score` term breaks ties when two bidders quote
155
+ identical self-scores — the higher-reputation bidder wins. This is
156
+ the property pinned by `tests/test_contract_net_reputation_scoring.py`.
157
+ """
158
+
159
+ def score(
160
+ self,
161
+ bid: Bid,
162
+ requirements: dict[str, Any],
163
+ reputation_kernel: ReputationKernel,
164
+ topic: str,
165
+ ) -> float:
166
+ rep_score = reputation_kernel.snapshot(bid.bidder_kid, topic).score
167
+ try:
168
+ calibration = reputation_kernel.calibration_adjustment(bid.bidder_kid, topic)
169
+ except Exception:
170
+ calibration = 1.0
171
+ weighted = rep_score * calibration * float(bid.bid_score_self)
172
+ return weighted + 0.05 * rep_score
173
+
174
+
175
+ # -----------------------------------------------------------------------
176
+ # Bidder-filter helpers
177
+ # -----------------------------------------------------------------------
178
+
179
+
180
+ def passes_bidder_filter(
181
+ bid: Bid,
182
+ bidder_filter: dict[str, Any] | None,
183
+ reputation_kernel: ReputationKernel,
184
+ topic: str,
185
+ ) -> tuple[bool, str | None]:
186
+ """Return (allowed, reject_rule_id).
187
+
188
+ The bidder_filter is an optional dict on the RFP_OPEN record. When
189
+ set, it carries:
190
+
191
+ - ``min_reputation`` (float in [0, 1]) — Sybil floor.
192
+ - ``topic`` (str) — must match the BID's RFP topic.
193
+
194
+ A bid that fails any predicate is rejected with rule_id
195
+ ``HM-CNP-BIDDER-FILTER`` (Sybil floor) — published as a BID_REJECT
196
+ record by the auctioneer before the AWARD is computed. This is the
197
+ SHOULD in spec §27.
198
+ """
199
+ if bidder_filter is None:
200
+ return True, None
201
+ min_rep = float(bidder_filter.get("min_reputation", 0.0))
202
+ if min_rep > 0.0:
203
+ score = reputation_kernel.snapshot(bid.bidder_kid, topic).score
204
+ if score < min_rep:
205
+ return False, "HM-CNP-BIDDER-FILTER"
206
+ return True, None
207
+
208
+
209
+ __all__ = [
210
+ "AWARD_DOMAIN_TAG",
211
+ "RFP",
212
+ "Award",
213
+ "Bid",
214
+ "BidScorer",
215
+ "ReputationWeightedScorer",
216
+ "award_preimage",
217
+ "passes_bidder_filter",
218
+ "topic_scope_for_rfp",
219
+ ]
@@ -0,0 +1,21 @@
1
+ """Cryptographic primitives — signing, hashing, HLC, domain tags.
2
+
3
+ v0.1 emits Ed25519 only. ML-DSA-65 hybrid emission gated to v0.2 but
4
+ `signature_alg` is in the signed COSE preimage from day 1 (A-ALG-BOUND).
5
+ """
6
+
7
+ from hypermind.crypto.domain import DomainTag, domain_separated_hash
8
+ from hypermind.crypto.hashing import blake3_32, sha256_kid
9
+ from hypermind.crypto.hlc import HLC, HLCSkewError
10
+ from hypermind.crypto.signing import Keypair, verify_ed25519
11
+
12
+ __all__ = [
13
+ "HLC",
14
+ "DomainTag",
15
+ "HLCSkewError",
16
+ "Keypair",
17
+ "blake3_32",
18
+ "domain_separated_hash",
19
+ "sha256_kid",
20
+ "verify_ed25519",
21
+ ]
@@ -0,0 +1,126 @@
1
+ """COSE_Encrypt0 (RFC 9052 §5.2) using HPKE-derived AEAD keys.
2
+
3
+ Single-recipient L3 envelope: a canonical CBOR `[protected, unprotected, ciphertext]`
4
+ where `protected` carries the HPKE suite id (`alg`) and recipient `kid`, while
5
+ `unprotected` carries the encapsulated key (`enc`). AAD is bound by the caller
6
+ and re-bound canonically over the protected header to mirror the A-ALG-BOUND
7
+ discipline of `wire.py`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+
14
+ import cbor2
15
+
16
+ from hypermind.crypto import hpke
17
+
18
+ # COSE header labels (RFC 9052 §3.1)
19
+ _LABEL_ALG = 1
20
+ _LABEL_KID = 4
21
+ # HyperMind-private label for HPKE encapsulated key in the unprotected map.
22
+ _LABEL_ENC = -1000
23
+
24
+
25
+ class CoseEncryptError(ValueError):
26
+ """Raised on malformed or non-canonical COSE_Encrypt0 input."""
27
+
28
+
29
+ def _det_cbor(obj: object) -> bytes:
30
+ return cbor2.dumps(obj, canonical=True)
31
+
32
+
33
+ def _kid_for_pub(recipient_pub: bytes) -> bytes:
34
+ return hashlib.sha256(recipient_pub).digest()
35
+
36
+
37
+ def _build_external_aad(protected_bytes: bytes, caller_aad: bytes) -> bytes:
38
+ # Bind the protected header bytes into the AEAD AAD so any tamper of the
39
+ # header (alg/kid) flips the tag. Mirrors `canonical_sig_input` in wire.py.
40
+ return _det_cbor([protected_bytes, caller_aad])
41
+
42
+
43
+ def encrypt(suite_id: int, recipient_pub: bytes, aad: bytes, plaintext: bytes) -> bytes:
44
+ """Encrypt `plaintext` to `recipient_pub` under HPKE suite `suite_id`.
45
+
46
+ Args:
47
+ suite_id: HPKE suite id (see `hpke.SUITE_*`).
48
+ recipient_pub: Recipient X25519 public key (32 bytes).
49
+ aad: Caller additional authenticated data, bound by the AEAD.
50
+ plaintext: Bytes to encrypt.
51
+
52
+ Returns:
53
+ Canonical CBOR-encoded `COSE_Encrypt0` 3-tuple `[protected: bstr,
54
+ unprotected: map, ciphertext: bstr]`.
55
+ """
56
+ protected_map: dict[int, object] = {
57
+ _LABEL_ALG: int(suite_id),
58
+ _LABEL_KID: _kid_for_pub(recipient_pub),
59
+ }
60
+ protected_bytes = _det_cbor(protected_map)
61
+
62
+ external_aad = _build_external_aad(protected_bytes, aad)
63
+ # HPKE `info` is empty for L3 envelope; suite_id + kid already domain-bound
64
+ # via the AEAD AAD.
65
+ enc, ct = hpke.seal(suite_id, recipient_pub, b"", external_aad, plaintext)
66
+
67
+ unprotected: dict[int, object] = {_LABEL_ENC: enc}
68
+ return _det_cbor([protected_bytes, unprotected, ct])
69
+
70
+
71
+ def decrypt(recipient_priv: bytes, cose_encrypt0_bytes: bytes, aad: bytes) -> bytes:
72
+ """Decrypt a COSE_Encrypt0 envelope.
73
+
74
+ Args:
75
+ recipient_priv: Recipient X25519 private key (32 bytes).
76
+ cose_encrypt0_bytes: Canonical CBOR output of `encrypt()`.
77
+ aad: Same caller AAD passed to `encrypt()`.
78
+
79
+ Returns:
80
+ Decrypted plaintext bytes.
81
+
82
+ Raises:
83
+ CoseEncryptError: malformed or non-canonical input.
84
+ cryptography.exceptions.InvalidTag: AEAD verification failed (tampered
85
+ ciphertext, AAD mismatch, or wrong recipient key).
86
+ """
87
+ try:
88
+ msg = cbor2.loads(cose_encrypt0_bytes)
89
+ except cbor2.CBORDecodeError as e:
90
+ raise CoseEncryptError(f"not valid CBOR: {e}") from e
91
+
92
+ if not isinstance(msg, list) or len(msg) != 3:
93
+ raise CoseEncryptError("COSE_Encrypt0 must be a 3-element array")
94
+ protected_bytes, unprotected, ciphertext = msg
95
+ if not isinstance(protected_bytes, bytes):
96
+ raise CoseEncryptError("protected must be bstr")
97
+ if not isinstance(unprotected, dict):
98
+ raise CoseEncryptError("unprotected must be a map")
99
+ if not isinstance(ciphertext, bytes):
100
+ raise CoseEncryptError("ciphertext must be bstr")
101
+
102
+ try:
103
+ protected = cbor2.loads(protected_bytes)
104
+ except cbor2.CBORDecodeError as e:
105
+ raise CoseEncryptError(f"protected header is not valid CBOR: {e}") from e
106
+ if not isinstance(protected, dict):
107
+ raise CoseEncryptError("protected header must be a CBOR map")
108
+
109
+ # Strict canonical re-encode gate (A-ALG-BOUND parity with wire.py).
110
+ if _det_cbor(protected) != protected_bytes:
111
+ raise CoseEncryptError("non-canonical protected header encoding")
112
+ if _det_cbor(msg) != cose_encrypt0_bytes:
113
+ raise CoseEncryptError("non-canonical COSE_Encrypt0 encoding")
114
+
115
+ if _LABEL_ALG not in protected or _LABEL_KID not in protected:
116
+ raise CoseEncryptError("protected header missing alg or kid")
117
+ suite_id = protected[_LABEL_ALG]
118
+ if not isinstance(suite_id, int):
119
+ raise CoseEncryptError("alg must be int")
120
+
121
+ enc = unprotected.get(_LABEL_ENC)
122
+ if not isinstance(enc, bytes):
123
+ raise CoseEncryptError("unprotected map missing HPKE enc bstr")
124
+
125
+ external_aad = _build_external_aad(protected_bytes, aad)
126
+ return hpke.open(suite_id, recipient_priv, enc, b"", external_aad, ciphertext)
@@ -0,0 +1,246 @@
1
+ """Pedersen distributed key generation (ROADMAP §0.2.3).
2
+
3
+ Implements the GJKR99 / Pedersen DKG protocol layer on Ed25519. The
4
+ output is a t-of-n share set whose group public key matches the FROST
5
+ trusted-dealer derivation in :mod:`hypermind.crypto.frost`, so an
6
+ existing FROST signer can consume DKG-derived shares without code
7
+ changes.
8
+
9
+ Protocol (3 rounds):
10
+
11
+ Round 1
12
+ Each participant *i* picks a random degree-(t-1) polynomial
13
+ ``f_i`` with ``f_i(0)`` as their secret-share contribution.
14
+ Broadcasts public commitments
15
+ ``C_i = [f_i.coeff[k] * G for k in 0..t-1]``.
16
+
17
+ Round 2
18
+ Sends ``s_{i→j} = f_i(j)`` privately to each *j*. Receivers verify
19
+ the share against ``C_i`` by checking
20
+ ``s_{i→j} * G == sum(j**k * C_i[k] for k in range(t))``.
21
+ Misbehaving participants are added to a complaint set.
22
+
23
+ Round 3
24
+ The qualifying set ``Q`` excludes any participant who failed share
25
+ verification or did not deliver. Each remaining participant
26
+ computes their final share ``s_i = sum(f_j(i) for j in Q)`` and
27
+ the group public key ``Y = sum(C_j[0] for j in Q)``.
28
+
29
+ Security properties:
30
+
31
+ * No single participant ever sees the group secret.
32
+ * As long as at least *t* honest participants remain in *Q*, the
33
+ group key is a valid (t, n) Shamir share of an unbiased random
34
+ secret in [0, L).
35
+ * Malicious participants are detected by share verification — every
36
+ receiver can publicly prove that participant *j* sent a bad share.
37
+
38
+ This module ships the **protocol layer**. Ceremony tooling (offline
39
+ coordinator, paper-tape transcripts, signed round receipts) lands at
40
+ v0.7 with the witness-onboarding work — see ROADMAP §0.7.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import secrets
46
+ from dataclasses import dataclass, field
47
+
48
+ from nacl.bindings import (
49
+ crypto_core_ed25519_add,
50
+ crypto_core_ed25519_scalar_add,
51
+ crypto_core_ed25519_scalar_mul,
52
+ crypto_scalarmult_ed25519_base_noclamp,
53
+ crypto_scalarmult_ed25519_noclamp,
54
+ )
55
+
56
+ from hypermind.crypto.frost import (
57
+ FrostShare,
58
+ L,
59
+ _validate_point,
60
+ _validate_scalar,
61
+ random_scalar,
62
+ scalar_from_int,
63
+ )
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class ParticipantState:
68
+ """Local state held by a single DKG participant.
69
+
70
+ Attributes:
71
+ index: 1-based participant id (matches FROST share index).
72
+ coefficients: List of *t* secret scalars (private; never leaves
73
+ the participant). ``coefficients[0]`` is the participant's
74
+ secret contribution to the group key.
75
+ commitments: List of *t* public points
76
+ ``coefficients[k] * G``. Broadcast at end of Round 1.
77
+ """
78
+
79
+ index: int
80
+ coefficients: tuple[bytes, ...]
81
+ commitments: tuple[bytes, ...]
82
+
83
+
84
+ @dataclass
85
+ class DkgSession:
86
+ """In-process Pedersen DKG coordinator.
87
+
88
+ The session orchestrates all three rounds for ``n`` simulated
89
+ participants. Real deployments split this across processes; the
90
+ in-process simulator is suitable for tests, sandboxes, and
91
+ pre-ceremony rehearsal.
92
+ """
93
+
94
+ threshold: int
95
+ participants: int
96
+ _states: dict[int, ParticipantState] = field(default_factory=dict)
97
+ # round-2 received shares: {receiver_index: {sender_index: scalar}}
98
+ _received: dict[int, dict[int, bytes]] = field(default_factory=dict)
99
+ # qualifying set after round 2 (None until round 2 completes)
100
+ _qualifying: set[int] | None = None
101
+
102
+ def __post_init__(self) -> None:
103
+ if not (1 <= self.threshold <= self.participants):
104
+ raise ValueError("require 1 <= threshold <= participants")
105
+ if self.participants < 1:
106
+ raise ValueError("participants must be >= 1")
107
+
108
+ # ---- round 1 ------------------------------------------------------
109
+
110
+ def round1(self, *, rng_seed: int | None = None) -> dict[int, tuple[bytes, ...]]:
111
+ """Generate every participant's polynomial + commitments.
112
+
113
+ Returns a public broadcast: ``{participant_index: commitments}``.
114
+ """
115
+ rand = secrets.SystemRandom() if rng_seed is None else None
116
+ for i in range(1, self.participants + 1):
117
+ coeffs = tuple(random_scalar() for _ in range(self.threshold))
118
+ commits = tuple(crypto_scalarmult_ed25519_base_noclamp(c) for c in coeffs)
119
+ self._states[i] = ParticipantState(index=i, coefficients=coeffs, commitments=commits)
120
+ self._received[i] = {}
121
+ _ = rand # unused; future hook for testable RNG injection
122
+ return {i: s.commitments for i, s in self._states.items()}
123
+
124
+ # ---- round 2 ------------------------------------------------------
125
+
126
+ def round2(
127
+ self,
128
+ *,
129
+ tampered_shares: dict[tuple[int, int], bytes] | None = None,
130
+ ) -> set[int]:
131
+ """Distribute private shares and verify them.
132
+
133
+ ``tampered_shares`` simulates a malicious participant: a
134
+ ``{(sender, receiver): bad_scalar}`` map replaces the otherwise
135
+ correct ``f_sender(receiver)`` with ``bad_scalar``. Receivers
136
+ detect the tampering via the public commitments and add the
137
+ sender to the complaint set.
138
+
139
+ Returns the qualifying set *Q* — participants whose shares
140
+ verified against their commitments.
141
+ """
142
+ if not self._states:
143
+ raise RuntimeError("call round1() before round2()")
144
+ complaints: set[int] = set()
145
+ tampered_shares = tampered_shares or {}
146
+
147
+ for sender_idx, sender in self._states.items():
148
+ for receiver_idx in range(1, self.participants + 1):
149
+ share = tampered_shares.get((sender_idx, receiver_idx)) or _evaluate_polynomial(
150
+ sender.coefficients, receiver_idx
151
+ )
152
+ # Verify share against sender's public commitments.
153
+ if not _verify_share(share, receiver_idx, sender.commitments):
154
+ complaints.add(sender_idx)
155
+ continue
156
+ self._received[receiver_idx][sender_idx] = share
157
+
158
+ qualifying = set(self._states.keys()) - complaints
159
+ if len(qualifying) < self.threshold:
160
+ raise ValueError(
161
+ f"DKG aborted: qualifying set size {len(qualifying)} below "
162
+ f"threshold {self.threshold} after {len(complaints)} complaint(s)"
163
+ )
164
+ self._qualifying = qualifying
165
+ return qualifying
166
+
167
+ # ---- round 3 ------------------------------------------------------
168
+
169
+ def finalize(self) -> tuple[bytes, list[FrostShare]]:
170
+ """Compute the group public key and per-participant FROST shares.
171
+
172
+ Only members of the qualifying set *Q* receive a final share.
173
+ The returned shares are interoperable with
174
+ :mod:`hypermind.crypto.frost` (same scalar/point encoding,
175
+ identical Lagrange reconstruction).
176
+ """
177
+ if self._qualifying is None:
178
+ raise RuntimeError("call round2() before finalize()")
179
+
180
+ # Group public key Y = sum_{j in Q} C_j[0]
181
+ group_pk: bytes | None = None
182
+ for j in sorted(self._qualifying):
183
+ c0 = self._states[j].commitments[0]
184
+ group_pk = c0 if group_pk is None else crypto_core_ed25519_add(group_pk, c0)
185
+ assert group_pk is not None
186
+ _validate_point(group_pk)
187
+
188
+ shares: list[FrostShare] = []
189
+ for i in sorted(self._qualifying):
190
+ # s_i = sum_{j in Q} f_j(i)
191
+ acc = scalar_from_int(0)
192
+ for j in sorted(self._qualifying):
193
+ share_ji = self._received[i].get(j)
194
+ if share_ji is None:
195
+ raise RuntimeError(f"participant {i} did not receive share from {j}")
196
+ acc = crypto_core_ed25519_scalar_add(acc, share_ji)
197
+ shares.append(FrostShare(index=i, secret=acc, group_public=group_pk))
198
+ return group_pk, shares
199
+
200
+
201
+ # ---- helpers ----------------------------------------------------------
202
+
203
+
204
+ def _evaluate_polynomial(coeffs: tuple[bytes, ...], x: int) -> bytes:
205
+ """Evaluate ``f(x) = sum_k coeffs[k] * x^k`` mod L."""
206
+ if x <= 0:
207
+ raise ValueError("polynomial evaluation point must be > 0")
208
+ x_pow = scalar_from_int(1)
209
+ x_step = scalar_from_int(x)
210
+ acc = scalar_from_int(0)
211
+ for k, a_k in enumerate(coeffs):
212
+ term = crypto_core_ed25519_scalar_mul(a_k, x_pow)
213
+ acc = crypto_core_ed25519_scalar_add(acc, term)
214
+ if k < len(coeffs) - 1:
215
+ x_pow = crypto_core_ed25519_scalar_mul(x_pow, x_step)
216
+ return acc
217
+
218
+
219
+ def _verify_share(share: bytes, receiver_idx: int, commitments: tuple[bytes, ...]) -> bool:
220
+ """Check ``share * G == sum_k receiver_idx^k * commitments[k]``."""
221
+ try:
222
+ _validate_scalar(share)
223
+ except ValueError:
224
+ return False
225
+
226
+ # LHS: share * G
227
+ lhs = crypto_scalarmult_ed25519_base_noclamp(share)
228
+
229
+ # RHS: sum_k (receiver_idx^k mod L) * commitments[k]
230
+ rhs: bytes | None = None
231
+ x_pow_int = 1
232
+ for _k, C_k in enumerate(commitments):
233
+ scalar = scalar_from_int(x_pow_int)
234
+ term = crypto_scalarmult_ed25519_noclamp(scalar, C_k)
235
+ rhs = term if rhs is None else crypto_core_ed25519_add(rhs, term)
236
+ x_pow_int = (x_pow_int * receiver_idx) % L
237
+ return lhs == rhs
238
+
239
+
240
+ def run_dkg(threshold: int, participants: int) -> tuple[bytes, list[FrostShare]]:
241
+ """One-shot helper: execute all three rounds with no malicious
242
+ participants. Returns ``(group_public_key, shares)``."""
243
+ session = DkgSession(threshold=threshold, participants=participants)
244
+ session.round1()
245
+ session.round2()
246
+ return session.finalize()
@@ -0,0 +1,59 @@
1
+ """Domain-separation tags (spec/02-cryptography.md §3.2).
2
+
3
+ Every cryptographic operation prefixes its preimage with a single byte
4
+ tag from this enum. Verifiers MUST reject any verification where the
5
+ expected tag does not match. This is the WF-DOMAIN refinement lemma —
6
+ it prevents cross-protocol signature substitution.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from enum import IntEnum
13
+
14
+
15
+ class DomainTag(IntEnum):
16
+ """Reserved domain-separation tags."""
17
+
18
+ ENVELOPE_SIGNATURE = 0x01
19
+ KID = 0x02
20
+ RID = 0x03 # receipt id
21
+ AGENT_CARD_CID = 0x04
22
+ RECEIPT_BODY = 0x05
23
+ ROTATION_RECEIPT = 0x06
24
+ SUCCESSION = 0x07
25
+ STH_LEAF = 0x08
26
+ COUNCIL_COSIG = 0x09
27
+ VC_ANCHOR = 0x0A
28
+ PROOF_RECORD = 0x0B
29
+ STATE_HASH = 0x0C
30
+ EMBED_RESP = 0x0D
31
+ MENTOR_SLASH = 0x0E
32
+ SEAL_REVOKE = 0x0F
33
+ CIVIC_REPUTATION_ENTRY = 0x10
34
+ MEMORIAL_BUMP = 0x11
35
+ # 0x12–0x17 — TAL (room_define, room_join, vouch, vouch_revoke,
36
+ # reputation_attestation, trust_query/reply)
37
+ TAL_ROOM_DEFINE = 0x12
38
+ TAL_ROOM_JOIN = 0x13
39
+ TAL_VOUCH = 0x14
40
+ TAL_VOUCH_REVOKE = 0x15
41
+ TAL_REPUTATION_ATTESTATION = 0x16
42
+ TAL_TRUST_QUERY = 0x17
43
+ # P2-16: KNOWS_DELTA gossip message type
44
+ KNOWS_DELTA = 0x18
45
+ # 0x19–0xFF reserved
46
+
47
+
48
+ def domain_separated_hash(tag: DomainTag, payload: bytes) -> bytes:
49
+ """SHA-256 over `tag || payload`. The single canonical hash op."""
50
+ h = hashlib.sha256()
51
+ h.update(bytes([int(tag)]))
52
+ h.update(payload)
53
+ return h.digest()
54
+
55
+
56
+ def reject_unallocated(tag_byte: int) -> None:
57
+ """Verifier MUST reject envelopes whose first byte is in 0x19..0xFF."""
58
+ if tag_byte > 0x18:
59
+ raise ValueError(f"unallocated domain tag 0x{tag_byte:02X}; reject envelope")