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,409 @@
1
+ """Transparency service + witness cosigning — real RFC 6962 Merkle.
2
+
3
+ Backed by `pymerkle.InmemoryTree` which produces standard RFC 6962
4
+ inclusion proofs. A receipt issuer commits to the current tree head
5
+ (STH = signed tree head); witnesses cosign the STH; verifiers can
6
+ later re-derive the leaf hash and validate the inclusion path against
7
+ the cosigned root.
8
+
9
+ Production deployments swap this for a CCF-backed SCITT TS or a
10
+ sigsum-tlog instance; the agent code does not change.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import hashlib
17
+ import warnings
18
+ from collections.abc import Iterable
19
+ from dataclasses import dataclass, field
20
+ from typing import Any
21
+
22
+ from pymerkle import InmemoryTree
23
+ from pymerkle import verify_inclusion as _pm_verify_inclusion
24
+
25
+ from hypermind.crypto.domain import DomainTag
26
+ from hypermind.crypto.hashing import sha256_with_tag
27
+ from hypermind.crypto.signing import Keypair
28
+ from hypermind.errors import ConformanceError
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class WitnessCosignature:
33
+ witness_kid: bytes
34
+ sth_root: bytes
35
+ tree_size: int
36
+ signature: bytes
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class PortableInclusionProof:
41
+ """RFC 6962 §2.1.1 inclusion proof — portable to any verifier.
42
+
43
+ Unlike `InclusionProof.proof_obj` (which carries a live pymerkle
44
+ `MerkleProof` instance), this representation is wire-portable: a
45
+ plain `(audit_path, leaf_index, tree_size)` tuple that any RFC 6962
46
+ verifier in any language can consume.
47
+
48
+ audit_path: list of 32-byte sibling hashes from leaf toward root.
49
+ leaf_index: 0-based leaf position in the tree.
50
+ tree_size: total leaf count at proof time.
51
+ """
52
+
53
+ audit_path: tuple[bytes, ...]
54
+ leaf_index: int
55
+ tree_size: int
56
+
57
+ def to_cbor(self) -> bytes:
58
+ import cbor2
59
+
60
+ return cbor2.dumps(
61
+ [list(self.audit_path), self.leaf_index, self.tree_size],
62
+ canonical=True,
63
+ )
64
+
65
+ @classmethod
66
+ def from_cbor(cls, data: bytes) -> PortableInclusionProof:
67
+ import cbor2
68
+
69
+ path, idx, size = cbor2.loads(data)
70
+ return cls(tuple(path), int(idx), int(size))
71
+
72
+
73
+ def _hash_internal(left: bytes, right: bytes) -> bytes:
74
+ """RFC 6962 §2.1 internal-node hash: H(0x01 || left || right)."""
75
+ return hashlib.sha256(b"\x01" + left + right).digest()
76
+
77
+
78
+ def verify_inclusion(leaf_hash: bytes, proof: PortableInclusionProof, root: bytes) -> bool:
79
+ """RFC 6962 §2.1.1 inclusion-proof verification — portable.
80
+
81
+ Implements the spec's reference algorithm verbatim so any third
82
+ party can re-implement against the wire bytes. Returns True iff the
83
+ audit path lifts `leaf_hash` to `root`.
84
+ """
85
+ if proof.tree_size <= 0 or proof.leaf_index < 0:
86
+ return False
87
+ if proof.leaf_index >= proof.tree_size:
88
+ return False
89
+
90
+ fn = proof.leaf_index
91
+ sn = proof.tree_size - 1
92
+ r = leaf_hash
93
+ for sibling in proof.audit_path:
94
+ if sn == 0:
95
+ # Path longer than the tree can justify.
96
+ return False
97
+ if fn % 2 == 1 or fn == sn:
98
+ r = _hash_internal(sibling, r)
99
+ while not (fn % 2 == 1 or fn == 0):
100
+ fn >>= 1
101
+ sn >>= 1
102
+ else:
103
+ r = _hash_internal(r, sibling)
104
+ fn >>= 1
105
+ sn >>= 1
106
+ return sn == 0 and r == root
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class InclusionProof:
111
+ """RFC 6962-shape inclusion proof.
112
+
113
+ Carries pymerkle's MerkleProof object plus the leaf hash and STH
114
+ root needed for offline verification. `tree_size` is the size at
115
+ the time the proof was generated; verifiers MUST cross-check that
116
+ the cosigned STH covers that exact size to detect equivocation.
117
+
118
+ `proof_obj` is an opaque pymerkle handle and is NOT portable across
119
+ implementations. Use `to_portable()` to obtain a wire-portable
120
+ `PortableInclusionProof` (RFC 6962 audit path + leaf index + tree
121
+ size) that any RFC 6962 verifier can consume.
122
+ """
123
+
124
+ leaf_index: int
125
+ leaf: bytes
126
+ leaf_hash: bytes # pymerkle's hashed-leaf form (hash(0x00 || leaf))
127
+ sth_root: bytes
128
+ tree_size: int
129
+ proof_obj: Any # pymerkle.MerkleProof — opaque, used by verifier
130
+
131
+ def to_portable(self) -> PortableInclusionProof:
132
+ """Convert pymerkle's proof object to the RFC 6962 portable form.
133
+
134
+ pymerkle's `path[0]` is the leaf hash itself; the remaining
135
+ entries are sibling hashes from leaf toward root (which is
136
+ exactly the RFC 6962 audit-path order).
137
+ """
138
+ pm_path = list(self.proof_obj.path)
139
+ # pymerkle path = [leaf_hash, sib_0, sib_1, ...]; siblings only.
140
+ audit_path = tuple(pm_path[1:]) if pm_path else ()
141
+ return PortableInclusionProof(
142
+ audit_path=audit_path,
143
+ leaf_index=self.leaf_index,
144
+ tree_size=self.tree_size,
145
+ )
146
+
147
+
148
+ @dataclass
149
+ class TransparencyService:
150
+ """In-process SCITT TS with real RFC 6962 Merkle tree."""
151
+
152
+ name: str = "hypermind-ts"
153
+ witness_keypairs: list[Keypair] = field(default_factory=list)
154
+ min_witness_quorum: int = 1
155
+ _tree: InmemoryTree = field(default_factory=lambda: InmemoryTree(algorithm="sha256"))
156
+ _leaf_index: dict[bytes, int] = field(default_factory=dict)
157
+ pending_reconciliation: bool = False
158
+ _last_seen_remote_size: int = 0
159
+ _gossip_events: list[dict[str, Any]] = field(default_factory=list)
160
+ _background_tasks: set[asyncio.Task[Any]] = field(default_factory=set)
161
+
162
+ async def gossip_sth(self, bus: Any, namespace_id: str) -> bytes:
163
+ """Publish (root, tree_size, cosigs) to /hm/{ns}/sth.
164
+
165
+ Serialised as canonical CBOR with tag ``STH_CROSS_GOSSIP`` and
166
+ signed by every configured witness. Returns the published bytes
167
+ (test hook). Silently no-ops when no witnesses are configured —
168
+ an unsigned STH offers no security value.
169
+ """
170
+ import cbor2
171
+
172
+ if not self.witness_keypairs:
173
+ return b""
174
+ root = self.current_root()
175
+ size = self.size()
176
+ cosigs = self._do_cosign(root, tree_size=size)
177
+ payload = {
178
+ "tag": "STH_CROSS_GOSSIP",
179
+ "root": root,
180
+ "tree_size": size,
181
+ "cosigs": [{"witness_kid": c.witness_kid, "signature": c.signature} for c in cosigs],
182
+ }
183
+ msg = cbor2.dumps(payload, canonical=True)
184
+ await bus.publish(f"/hm/{namespace_id}/sth", msg)
185
+ return msg
186
+
187
+ def receive_sth(self, sth_msg: bytes, sender_kid: bytes) -> bool:
188
+ """Verify a gossiped STH; flag pending_reconciliation if larger.
189
+
190
+ Returns True iff at least one cosignature verified AND the
191
+ received tree_size exceeds our local size.
192
+ """
193
+ import cbor2
194
+
195
+ try:
196
+ payload = cbor2.loads(sth_msg)
197
+ except Exception:
198
+ return False
199
+ if not isinstance(payload, dict) or payload.get("tag") != "STH_CROSS_GOSSIP":
200
+ return False
201
+ root = payload.get("root")
202
+ size = payload.get("tree_size")
203
+ cosigs = payload.get("cosigs") or []
204
+ if not isinstance(root, (bytes, bytearray)) or not isinstance(size, int):
205
+ return False
206
+ verified_count = 0
207
+ for entry in cosigs:
208
+ cosig = WitnessCosignature(
209
+ witness_kid=bytes(entry["witness_kid"]),
210
+ sth_root=bytes(root),
211
+ tree_size=int(size),
212
+ signature=bytes(entry["signature"]),
213
+ )
214
+ if self.verify_cosignature(cosig):
215
+ verified_count += 1
216
+ if verified_count < max(1, self.min_witness_quorum):
217
+ return False
218
+ self._last_seen_remote_size = max(self._last_seen_remote_size, int(size))
219
+ if int(size) > self.size():
220
+ self.pending_reconciliation = True
221
+ self._gossip_events.append(
222
+ {
223
+ "event": "sth_remote_ahead",
224
+ "remote_size": int(size),
225
+ "local_size": self.size(),
226
+ "sender_kid": sender_kid,
227
+ }
228
+ )
229
+ return True
230
+ return False
231
+
232
+ def append(self, statement_kid: bytes) -> InclusionProof:
233
+ """Append a statement kid as a Merkle leaf and return inclusion proof."""
234
+ if len(statement_kid) != 32:
235
+ raise ConformanceError("statement_kid must be 32 bytes")
236
+ # RFC 6962 §2.1: leaves are hashed with prefix 0x00 to prevent
237
+ # second-preimage attacks (collision between leaf and inner-node).
238
+ # pymerkle's InmemoryTree applies the same prefix internally when
239
+ # `disable_security=False` (default).
240
+ self._tree.append_entry(statement_kid)
241
+ size = self._tree.get_size()
242
+ leaf_idx = size - 1
243
+ self._leaf_index[statement_kid] = leaf_idx
244
+ proof = self._tree.prove_inclusion(leaf_idx + 1, size)
245
+ # pymerkle hashes leaves with the RFC 6962 0x00 prefix internally.
246
+ leaf_hash = hashlib.sha256(b"\x00" + statement_kid).digest()
247
+ return InclusionProof(
248
+ leaf_index=leaf_idx,
249
+ leaf=statement_kid,
250
+ leaf_hash=leaf_hash,
251
+ sth_root=bytes(self._tree.get_state()),
252
+ tree_size=size,
253
+ proof_obj=proof,
254
+ )
255
+
256
+ def witness_cosign(
257
+ self,
258
+ sth_root: bytes,
259
+ *,
260
+ tree_size: int | None = None,
261
+ witnesses: Iterable[Keypair] | None = None,
262
+ ) -> list[WitnessCosignature]:
263
+ """k-of-n witness cosignatures over the (root, size) pair.
264
+
265
+ Binding `tree_size` into the signed payload is the equivocation
266
+ defence: a witness cannot sign two roots with the same size.
267
+
268
+ IMPORTANT (closes self-heal Cycle-1 Finding #6): callers MUST
269
+ pass the `tree_size` that pairs with `sth_root` (use
270
+ `proof.tree_size` from the InclusionProof returned by
271
+ `append()`). If `tree_size` is omitted we read the live tree
272
+ size, which is correct only when no concurrent appends can
273
+ occur — that's a hard precondition we cannot enforce, so the
274
+ caller is responsible.
275
+
276
+ .. deprecated::
277
+ Use ``append_and_cosign()`` instead. Calling this method
278
+ directly is a footgun if any append occurs between the
279
+ ``append()`` call and this call, because the tree size will
280
+ no longer pair with the root you're signing.
281
+ """
282
+ warnings.warn(
283
+ "witness_cosign() is deprecated and will be removed in v1.0. "
284
+ "Use append_and_cosign() to atomically append and cosign. "
285
+ "Calling witness_cosign() standalone is unsafe under concurrent appends.",
286
+ DeprecationWarning,
287
+ stacklevel=2,
288
+ )
289
+ return self._do_cosign(sth_root, tree_size=tree_size, witnesses=witnesses)
290
+
291
+ def _do_cosign(
292
+ self,
293
+ sth_root: bytes,
294
+ *,
295
+ tree_size: int | None = None,
296
+ witnesses: Iterable[Keypair] | None = None,
297
+ ) -> list[WitnessCosignature]:
298
+ """Internal cosign — same as :meth:`witness_cosign` but no
299
+ deprecation warning. Used by :meth:`append_and_cosign`."""
300
+ chosen = list(witnesses) if witnesses is not None else self.witness_keypairs
301
+ if len(chosen) < self.min_witness_quorum:
302
+ raise ConformanceError(
303
+ f"witness quorum {self.min_witness_quorum} not reachable (have {len(chosen)})"
304
+ )
305
+ size = tree_size if tree_size is not None else self._tree.get_size()
306
+ cosigs: list[WitnessCosignature] = []
307
+ for w in chosen:
308
+ preimage = _witness_signing_input(sth_root, size, w.kid)
309
+ cosigs.append(
310
+ WitnessCosignature(
311
+ witness_kid=w.kid,
312
+ sth_root=sth_root,
313
+ tree_size=size,
314
+ signature=w.sign(preimage),
315
+ )
316
+ )
317
+ return cosigs
318
+
319
+ def append_and_cosign(
320
+ self,
321
+ statement_kid: bytes,
322
+ *,
323
+ witnesses: Iterable[Keypair] | None = None,
324
+ ) -> tuple[InclusionProof, list[WitnessCosignature]]:
325
+ """Atomic append + cosign. Use this whenever concurrent appends
326
+ are possible — guarantees the cosignatures bind the size paired
327
+ with the appended leaf's root."""
328
+ proof = self.append(statement_kid)
329
+ cosigs = self._do_cosign(proof.sth_root, tree_size=proof.tree_size, witnesses=witnesses)
330
+ bus = getattr(self, "_gossip_bus", None)
331
+ ns = getattr(self, "_gossip_namespace", None)
332
+ if bus is not None and ns is not None:
333
+ try:
334
+ loop = asyncio.get_running_loop()
335
+ task = loop.create_task(self.gossip_sth(bus, ns))
336
+ self._background_tasks.add(task)
337
+ task.add_done_callback(self._background_tasks.discard)
338
+ except RuntimeError:
339
+ pass
340
+ return proof, cosigs
341
+
342
+ def attach_gossip(self, bus: Any, namespace_id: str) -> None:
343
+ """Wire a bus + namespace so subsequent append_and_cosign() fires
344
+ gossip_sth() automatically. No-op when no event loop is running."""
345
+ self._gossip_bus = bus
346
+ self._gossip_namespace = namespace_id
347
+
348
+ def current_root(self) -> bytes:
349
+ return bytes(self._tree.get_state())
350
+
351
+ def size(self) -> int:
352
+ return self._tree.get_size()
353
+
354
+ def verify_inclusion(self, proof: InclusionProof, *, expected_kid: bytes | None = None) -> bool:
355
+ """Verify via pymerkle's RFC 6962 inclusion-proof verifier."""
356
+ if expected_kid is not None and proof.leaf != expected_kid:
357
+ return False
358
+ try:
359
+ _pm_verify_inclusion(proof.leaf_hash, proof.sth_root, proof.proof_obj)
360
+ return True
361
+ except Exception:
362
+ return False
363
+
364
+ def verify_cosignature(self, cosig: WitnessCosignature) -> bool:
365
+ """Verify a witness cosignature over (root, size, witness_kid).
366
+
367
+ Handles both classical Ed25519 (64-byte sig) and composite
368
+ ML-DSA-65 + Ed25519 sigs (LAMPS §6 — first 64 bytes are
369
+ Ed25519 over the LAMPS preimage). witness_kid is always the
370
+ Ed25519 half (kept stable through alg rotation by design),
371
+ so Ed25519 verification of the prefix binds the witness's
372
+ attestation. PQ-side verify is future hardening.
373
+
374
+ Used by I6 receive-side cosig verification on subscribe.
375
+ """
376
+ from hypermind.crypto.signing import verify_ed25519
377
+
378
+ sig = cosig.signature
379
+ if not sig:
380
+ return False
381
+ preimage = _witness_signing_input(
382
+ cosig.sth_root,
383
+ cosig.tree_size,
384
+ cosig.witness_kid,
385
+ )
386
+ if len(sig) == 64:
387
+ return verify_ed25519(cosig.witness_kid, sig, preimage)
388
+ # Composite path: Ed25519 prefix signed over LAMPS preimage.
389
+ try:
390
+ from hypermind.crypto.signing import _lamps_preimage # type: ignore
391
+
392
+ return verify_ed25519(
393
+ cosig.witness_kid,
394
+ sig[:64],
395
+ _lamps_preimage(preimage),
396
+ )
397
+ except Exception:
398
+ return False
399
+
400
+
401
+ def _witness_signing_input(sth_root: bytes, tree_size: int, witness_kid: bytes) -> bytes:
402
+ """Domain-separated input for witness STH signature.
403
+
404
+ Binds `(root, size, witness_kid)` so a witness cannot equivocate by
405
+ signing two different roots at the same size, and cross-witness
406
+ forwarding cannot fool a verifier.
407
+ """
408
+ payload = sth_root + tree_size.to_bytes(8, "big") + witness_kid
409
+ return sha256_with_tag(DomainTag.STH_LEAF, payload)
@@ -0,0 +1,60 @@
1
+ """MCP integration — JSON-RPC 2.0 transport, server, client, and the
2
+ legacy verb-bridge module.
3
+
4
+ v0.10.1 ships the full transport stack (T1):
5
+
6
+ MCPServer / MCPClient — JSON-RPC 2.0 over stdio (and WebSocket
7
+ when the optional `websockets` extra is
8
+ installed; deferred to v0.10.2 otherwise).
9
+ StdioTransport — line-delimited JSON over asyncio streams.
10
+ Provenance — T1-03 CapToken-bound tool-call attestation.
11
+
12
+ The earlier verb-bridge classes (``HyperMindMCPServer``,
13
+ ``sealed_mcp_client``) remain importable from ``hypermind.mcp.bridge``
14
+ and from this package, so existing user code continues to work.
15
+ """
16
+
17
+ from hypermind.mcp.bridge import HyperMindMCPServer, sealed_mcp_client
18
+ from hypermind.mcp.client import MCPClient, MCPRpcError
19
+ from hypermind.mcp.provenance import (
20
+ ERR_PROVENANCE_INVALID,
21
+ ERR_PROVENANCE_MISSING,
22
+ ERR_PROVENANCE_REQUEST_KID_MISMATCH,
23
+ ERR_PROVENANCE_UNKNOWN_CAP,
24
+ Provenance,
25
+ ProvenanceError,
26
+ compute_cap_token_root,
27
+ compute_request_kid,
28
+ sign_provenance,
29
+ verify_provenance,
30
+ )
31
+ from hypermind.mcp.server import MCPServer, ToolDescriptor
32
+ from hypermind.mcp.transport import (
33
+ MCP_SCHEMA_VERSION,
34
+ MCPTransport,
35
+ StdioTransport,
36
+ WebSocketTransport,
37
+ )
38
+
39
+ __all__ = [
40
+ "ERR_PROVENANCE_INVALID",
41
+ "ERR_PROVENANCE_MISSING",
42
+ "ERR_PROVENANCE_REQUEST_KID_MISMATCH",
43
+ "ERR_PROVENANCE_UNKNOWN_CAP",
44
+ "MCP_SCHEMA_VERSION",
45
+ "HyperMindMCPServer",
46
+ "MCPClient",
47
+ "MCPRpcError",
48
+ "MCPServer",
49
+ "MCPTransport",
50
+ "Provenance",
51
+ "ProvenanceError",
52
+ "StdioTransport",
53
+ "ToolDescriptor",
54
+ "WebSocketTransport",
55
+ "compute_cap_token_root",
56
+ "compute_request_kid",
57
+ "sealed_mcp_client",
58
+ "sign_provenance",
59
+ "verify_provenance",
60
+ ]
@@ -0,0 +1,151 @@
1
+ """MCP bridge — two patterns:
2
+
3
+ 1. `HyperMindMCPServer(agent, expose=[...])` — expose agent verbs as
4
+ MCP tools. Each invocation is gated by the agent's CapToken
5
+ caveat-chain (closes Cycle-1 fix #5 / MCP confused-deputy gap).
6
+
7
+ 2. `sealed_mcp_client(agent, "stdio://...")` — wrap an external MCP
8
+ server so every tool call's input + output is sealed into a
9
+ HyperMind signed statement, providing non-repudiable provenance for
10
+ tool-mediated claims.
11
+
12
+ The implementation here is the contract; importing the real MCP runtime
13
+ is deferred until the SDK is extended with the `[mcp]` extra installed.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import contextlib
19
+ from collections.abc import Awaitable, Callable
20
+ from dataclasses import dataclass
21
+ from typing import Any
22
+
23
+ import cbor2
24
+
25
+ from hypermind.agent import HyperMindAgent
26
+ from hypermind.crypto.hashing import blake3_32
27
+ from hypermind.wire import Kid
28
+
29
+ ToolHandler = Callable[[dict[str, Any]], Awaitable[Any]]
30
+
31
+
32
+ @dataclass
33
+ class HyperMindMCPServer:
34
+ """Expose agent verbs as MCP tools. v0.1 ships the dispatch logic;
35
+ binding to the actual MCP transport is done by the consumer.
36
+
37
+ Usage:
38
+ server = HyperMindMCPServer(agent, expose=["publish_claim", "dispute"])
39
+ # then plug `server.tools` into your MCP transport (stdio_server etc).
40
+ """
41
+
42
+ agent: HyperMindAgent
43
+ expose: list[str]
44
+
45
+ def __post_init__(self) -> None:
46
+ # Validate that every requested tool exists.
47
+ unknown = [t for t in self.expose if not hasattr(self.agent, t)]
48
+ if unknown:
49
+ raise ValueError(f"unknown MCP tool(s): {unknown}")
50
+
51
+ @property
52
+ def tools(self) -> dict[str, ToolHandler]:
53
+ """Map of tool_name -> async handler.
54
+
55
+ Wire these into your MCP transport (e.g. mcp.server.stdio_server).
56
+ """
57
+ return {name: self._make_handler(name) for name in self.expose}
58
+
59
+ def _make_handler(self, name: str) -> ToolHandler:
60
+ method = getattr(self.agent, name)
61
+
62
+ async def _handler(args: dict[str, Any]) -> Any:
63
+ # Cap-token enforcement happens inside the verb itself, so
64
+ # an MCP caller without authority gets `AuthError` with
65
+ # `failed_caveat` populated.
66
+ return await method(**args)
67
+
68
+ _handler.__name__ = f"hypermind_mcp_{name}"
69
+ return _handler
70
+
71
+
72
+ @dataclass
73
+ class _SealedToolResult:
74
+ tool_name: str
75
+ tool_server_id: str
76
+ input_hash: bytes
77
+ output_hash: bytes
78
+ output: Any
79
+ input_kid: Kid | None = None
80
+
81
+ def as_claim(self) -> dict:
82
+ """Convert the wrapped result into a publish_claim payload."""
83
+ return {
84
+ "tool_name": self.tool_name,
85
+ "tool_server_id": self.tool_server_id,
86
+ "input_hash": self.input_hash.hex(),
87
+ "output_hash": self.output_hash.hex(),
88
+ "output": self.output,
89
+ }
90
+
91
+
92
+ @contextlib.asynccontextmanager
93
+ async def sealed_mcp_client(agent: HyperMindAgent, endpoint: str):
94
+ """Async context manager wrapping an external MCP server.
95
+
96
+ For v0.1 sandbox use this returns a stub client whose `call_tool`
97
+ seals input+output hashes; production wiring of the MCP transport
98
+ is provided by the `[mcp]` extra.
99
+ """
100
+ yield _SealedMCPClient(agent=agent, endpoint=endpoint)
101
+
102
+
103
+ @dataclass
104
+ class _SealedMCPClient:
105
+ agent: HyperMindAgent
106
+ endpoint: str
107
+
108
+ async def call_tool(
109
+ self,
110
+ tool_name: str,
111
+ args: dict[str, Any],
112
+ *,
113
+ execute: Callable[[str, dict[str, Any]], Awaitable[Any]] | None = None,
114
+ ) -> _SealedToolResult:
115
+ """Call `tool_name` on the wrapped MCP server, sealing the result.
116
+
117
+ `execute` is a hook for tests/sandboxes; production wiring will
118
+ invoke the actual MCP server.
119
+ """
120
+ if execute is None:
121
+ # Sandbox fallback: echo the args as the result.
122
+ output: Any = {"echo": args}
123
+ else:
124
+ output = await execute(tool_name, args)
125
+ # Canonical CBOR hashing — `repr()` was non-stable across
126
+ # Python versions and dict-insertion orders, so two equivalent
127
+ # calls could hash differently (closes self-heal Cycle-1 honourable
128
+ # mention on `repr()`-based provenance).
129
+ input_hash = blake3_32(_canonical_bytes(args))
130
+ output_hash = blake3_32(_canonical_bytes(output))
131
+ return _SealedToolResult(
132
+ tool_name=tool_name,
133
+ tool_server_id=self.endpoint,
134
+ input_hash=input_hash,
135
+ output_hash=output_hash,
136
+ output=output,
137
+ )
138
+
139
+
140
+ def _canonical_bytes(obj: Any) -> bytes:
141
+ """Deterministic CBOR encoding of an MCP tool arg/result.
142
+
143
+ Falls back to `repr()` only for non-CBOR-encodable objects (custom
144
+ classes), but logs a marker so the caller knows the hash is not
145
+ reproducible across Python versions.
146
+ """
147
+ try:
148
+ return cbor2.dumps(obj, canonical=True)
149
+ except (TypeError, ValueError):
150
+ # Last-resort: stringify deterministically using sorted-repr.
151
+ return ("non-cbor:" + repr(obj)).encode()