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.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|
hypermind/mcp/bridge.py
ADDED
|
@@ -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()
|