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,142 @@
1
+ """MCP client — JSON-RPC 2.0 caller with CapToken provenance signing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import itertools
7
+ from typing import Any
8
+
9
+ from hypermind.crypto.signing import Keypair
10
+ from hypermind.mcp.provenance import (
11
+ Provenance,
12
+ ProvenanceError,
13
+ sign_provenance,
14
+ )
15
+ from hypermind.mcp.transport import MCPTransport
16
+
17
+
18
+ class MCPRpcError(Exception):
19
+ """Raised when the server returns a JSON-RPC error envelope."""
20
+
21
+ def __init__(self, code: int, message: str) -> None:
22
+ super().__init__(f"[{code}] {message}")
23
+ self.code = code
24
+ self.message = message
25
+
26
+
27
+ class MCPClient:
28
+ """Async JSON-RPC 2.0 client over an :class:`MCPTransport`.
29
+
30
+ Concurrency model: a single background task drains incoming frames
31
+ and routes them to per-request :class:`asyncio.Future` slots keyed
32
+ by ``id``. ``call`` is therefore safe to invoke concurrently from
33
+ multiple coroutines on the same client.
34
+ """
35
+
36
+ def __init__(self, transport: MCPTransport) -> None:
37
+ self._transport = transport
38
+ self._counter = itertools.count(1)
39
+ self._pending: dict[int, asyncio.Future[Any]] = {}
40
+ self._reader_task: asyncio.Task | None = None
41
+ self._closed = False
42
+
43
+ async def __aenter__(self) -> MCPClient:
44
+ await self.start()
45
+ return self
46
+
47
+ async def __aexit__(self, *_exc: Any) -> None:
48
+ await self.close()
49
+
50
+ async def start(self) -> None:
51
+ if self._reader_task is None:
52
+ self._reader_task = asyncio.create_task(self._reader_loop())
53
+
54
+ async def _reader_loop(self) -> None:
55
+ try:
56
+ while not self._closed:
57
+ msg = await self._transport.recv()
58
+ msg_id = msg.get("id")
59
+ if not isinstance(msg_id, int):
60
+ continue
61
+ fut = self._pending.pop(msg_id, None)
62
+ if fut is None or fut.done():
63
+ continue
64
+ if "error" in msg:
65
+ err = msg["error"]
66
+ fut.set_exception(
67
+ MCPRpcError(int(err.get("code", 0)), str(err.get("message", "")))
68
+ )
69
+ else:
70
+ fut.set_result(msg.get("result"))
71
+ except (ConnectionError, asyncio.CancelledError):
72
+ pass
73
+ finally:
74
+ for fut in self._pending.values():
75
+ if not fut.done():
76
+ fut.set_exception(ConnectionError("MCP transport closed"))
77
+ self._pending.clear()
78
+
79
+ async def call(self, method: str, params: dict[str, Any] | None = None) -> Any:
80
+ """Issue a JSON-RPC request and await the response."""
81
+ if self._reader_task is None:
82
+ await self.start()
83
+ msg_id = next(self._counter)
84
+ fut: asyncio.Future[Any] = asyncio.get_event_loop().create_future()
85
+ self._pending[msg_id] = fut
86
+ await self._transport.send(
87
+ {"jsonrpc": "2.0", "id": msg_id, "method": method, "params": params or {}}
88
+ )
89
+ return await fut
90
+
91
+ async def initialize(self) -> dict[str, Any]:
92
+ return await self.call("initialize", {})
93
+
94
+ async def list_tools(self) -> list[dict[str, Any]]:
95
+ result = await self.call("tools/list", {})
96
+ return list(result.get("tools", []))
97
+
98
+ async def call_tool(
99
+ self,
100
+ name: str,
101
+ arguments: dict[str, Any],
102
+ *,
103
+ signing_keypair: Keypair,
104
+ cap_token_root: bytes,
105
+ provenance_override: Provenance | None = None,
106
+ ) -> Any:
107
+ """Call ``tools/call`` with a CapToken-bound provenance header.
108
+
109
+ ``provenance_override`` is an escape hatch for negative tests
110
+ that want to inject a forged provenance object — production
111
+ callers should never pass it.
112
+ """
113
+ params: dict[str, Any] = {"name": name, "arguments": arguments}
114
+ if provenance_override is not None:
115
+ provenance = provenance_override
116
+ else:
117
+ provenance = sign_provenance(
118
+ keypair=signing_keypair,
119
+ method="tools/call",
120
+ params=params,
121
+ cap_token_root=cap_token_root,
122
+ )
123
+ params_with_prov = dict(params)
124
+ params_with_prov["provenance"] = provenance.to_dict()
125
+ return await self.call("tools/call", params_with_prov)
126
+
127
+ async def close(self) -> None:
128
+ if self._closed:
129
+ return
130
+ self._closed = True
131
+ if self._reader_task is not None:
132
+ self._reader_task.cancel()
133
+ try:
134
+ await self._reader_task
135
+ except (asyncio.CancelledError, Exception):
136
+ pass
137
+ await self._transport.close()
138
+
139
+
140
+ # Re-export ProvenanceError so callers handling the negative path do
141
+ # not need to import it from hypermind.mcp.provenance directly.
142
+ __all__ = ["MCPClient", "MCPRpcError", "ProvenanceError"]
@@ -0,0 +1,214 @@
1
+ """MCP tool-call provenance binding (T1-03).
2
+
3
+ Every MCP ``tools/call`` request signed by the calling agent MUST
4
+ carry a *provenance* triple in its protected header:
5
+
6
+ calling_agent_kid : bstr (32-byte Ed25519 pubkey half)
7
+ cap_token_root : bstr (SHA-256 of the root CapToken authorising
8
+ the call — for fast lookup against the
9
+ namespace-scoped cap store)
10
+ request_kid : bstr (SHA-256 of the canonical CBOR encoding of
11
+ the request body, sans signature — used
12
+ for idempotency)
13
+
14
+ The triple is bound to the call by an Ed25519 signature over the
15
+ canonical CBOR encoding of ``[method, params_without_provenance,
16
+ calling_agent_kid, cap_token_root, request_kid]``. The MCP server
17
+ verifies the signature *before* dispatching the tool, mirroring the
18
+ A-ALG-BOUND discipline of :mod:`hypermind.wire`.
19
+
20
+ Wire label
21
+ ----------
22
+ The numeric label ``HM_MCP_PROVENANCE = -65627`` is reserved in
23
+ :mod:`hypermind.wire` so every label allocation lives in one place.
24
+ On JSON-RPC the field is named ``provenance`` (camelCase) inside the
25
+ request ``params``; on a future COSE_Sign1 carrier the label numeric
26
+ value is the same.
27
+
28
+ Error codes (JSON-RPC 2.0 ``error.code``)
29
+ -----------------------------------------
30
+ - ``-32001`` MCP_PROVENANCE_MISSING — request lacked provenance
31
+ - ``-32002`` MCP_PROVENANCE_INVALID — signature check failed
32
+ - ``-32003`` MCP_PROVENANCE_UNKNOWN_CAP — cap_token_root not registered
33
+ - ``-32004`` MCP_PROVENANCE_REQUEST_KID_MISMATCH — request_kid does not
34
+ match the canonical hash of the params
35
+
36
+ Codes ``-32000..-32099`` are reserved for server errors by the
37
+ JSON-RPC 2.0 specification (§5.1); we use the lower end of that range
38
+ so custom codes do not collide with the library's pre-allocated
39
+ ``-32600..-32603`` (parse / invalid-request / method-not-found /
40
+ invalid-params).
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import hashlib
46
+ from dataclasses import dataclass
47
+ from typing import Any
48
+
49
+ import cbor2
50
+
51
+ from hypermind.crypto.signing import Keypair, verify_ed25519
52
+
53
+ #: JSON-RPC error codes for provenance failures.
54
+ ERR_PROVENANCE_MISSING = -32001
55
+ ERR_PROVENANCE_INVALID = -32002
56
+ ERR_PROVENANCE_UNKNOWN_CAP = -32003
57
+ ERR_PROVENANCE_REQUEST_KID_MISMATCH = -32004
58
+
59
+
60
+ def _canon(obj: Any) -> bytes:
61
+ """RFC 8949 §4.2 deterministic CBOR encoding."""
62
+ return cbor2.dumps(obj, canonical=True)
63
+
64
+
65
+ def compute_request_kid(method: str, params_without_provenance: dict[str, Any]) -> bytes:
66
+ """SHA-256 of canonical CBOR(method, params)."""
67
+ return hashlib.sha256(_canon([method, params_without_provenance])).digest()
68
+
69
+
70
+ def compute_cap_token_root(cap_token_bytes: bytes) -> bytes:
71
+ """SHA-256 of the root CapToken's canonical bytes."""
72
+ return hashlib.sha256(cap_token_bytes).digest()
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class Provenance:
77
+ """Bound triple + signature attached to an MCP request."""
78
+
79
+ calling_agent_kid: bytes
80
+ cap_token_root: bytes
81
+ request_kid: bytes
82
+ signature: bytes
83
+
84
+ def to_dict(self) -> dict[str, str]:
85
+ """Serialise to a JSON-safe mapping (hex-encoded byte fields)."""
86
+ return {
87
+ "calling_agent_kid": self.calling_agent_kid.hex(),
88
+ "cap_token_root": self.cap_token_root.hex(),
89
+ "request_kid": self.request_kid.hex(),
90
+ "signature": self.signature.hex(),
91
+ }
92
+
93
+ @classmethod
94
+ def from_dict(cls, m: dict[str, Any]) -> Provenance:
95
+ try:
96
+ return cls(
97
+ calling_agent_kid=bytes.fromhex(m["calling_agent_kid"]),
98
+ cap_token_root=bytes.fromhex(m["cap_token_root"]),
99
+ request_kid=bytes.fromhex(m["request_kid"]),
100
+ signature=bytes.fromhex(m["signature"]),
101
+ )
102
+ except (KeyError, TypeError, ValueError) as e:
103
+ raise ProvenanceError(
104
+ ERR_PROVENANCE_INVALID, f"malformed provenance object: {e}"
105
+ ) from e
106
+
107
+
108
+ class ProvenanceError(Exception):
109
+ """Raised by the MCP server when provenance verification fails."""
110
+
111
+ def __init__(self, code: int, message: str) -> None:
112
+ super().__init__(message)
113
+ self.code = code
114
+ self.message = message
115
+
116
+
117
+ def sign_provenance(
118
+ *,
119
+ keypair: Keypair,
120
+ method: str,
121
+ params: dict[str, Any],
122
+ cap_token_root: bytes,
123
+ ) -> Provenance:
124
+ """Build a provenance attestation for an outbound MCP request.
125
+
126
+ ``params`` must be the request params *without* a ``provenance``
127
+ field — the caller is signing the bare semantic payload.
128
+ """
129
+ if "provenance" in params:
130
+ raise ValueError("params must not yet contain a 'provenance' field")
131
+ request_kid = compute_request_kid(method, params)
132
+ sig_input = _canon(
133
+ [
134
+ "MCP/Provenance/v1",
135
+ method,
136
+ params,
137
+ keypair.kid,
138
+ cap_token_root,
139
+ request_kid,
140
+ ]
141
+ )
142
+ signature = keypair.sign(sig_input)
143
+ return Provenance(
144
+ calling_agent_kid=keypair.kid,
145
+ cap_token_root=cap_token_root,
146
+ request_kid=request_kid,
147
+ signature=signature,
148
+ )
149
+
150
+
151
+ def check_provenance(headers: dict) -> None:
152
+ """Validate that *headers* contains the required MCP provenance fields.
153
+
154
+ This is the fast-path gate applied to inbound MCP tool-call envelopes
155
+ before cryptographic verification. It enforces spec §4.13.1: every
156
+ inbound call MUST carry ``cap_token_root`` (and, by extension, the full
157
+ provenance triple). Missing or ``None`` values raise immediately so the
158
+ server can return ``ERR_PROVENANCE_MISSING`` without attempting to parse
159
+ a broken envelope.
160
+
161
+ Parameters
162
+ ----------
163
+ headers:
164
+ The request headers / params dict received from the MCP client.
165
+ Typically the ``provenance`` sub-dict extracted from the JSON-RPC
166
+ ``params`` object.
167
+
168
+ Raises
169
+ ------
170
+ ValueError
171
+ ``"MCP-NO-CAP-ROOT"`` when ``cap_token_root`` is absent or ``None``.
172
+ ValueError
173
+ ``"MCP-NO-CALLING-AGENT-KID"`` when ``calling_agent_kid`` is absent.
174
+ ValueError
175
+ ``"MCP-NO-REQUEST-KID"`` when ``request_kid`` is absent.
176
+ """
177
+ if not headers.get("cap_token_root"):
178
+ raise ValueError("MCP-NO-CAP-ROOT")
179
+ if not headers.get("calling_agent_kid"):
180
+ raise ValueError("MCP-NO-CALLING-AGENT-KID")
181
+ if not headers.get("request_kid"):
182
+ raise ValueError("MCP-NO-REQUEST-KID")
183
+
184
+
185
+ def verify_provenance(
186
+ *,
187
+ method: str,
188
+ params_without_provenance: dict[str, Any],
189
+ provenance: Provenance,
190
+ ) -> None:
191
+ """Verify the signature and request_kid binding.
192
+
193
+ Raises :class:`ProvenanceError` with a JSON-RPC error code on
194
+ failure. Does NOT verify that ``cap_token_root`` corresponds to a
195
+ known CapToken — the server does that in a second step.
196
+ """
197
+ expected_kid = compute_request_kid(method, params_without_provenance)
198
+ if expected_kid != provenance.request_kid:
199
+ raise ProvenanceError(
200
+ ERR_PROVENANCE_REQUEST_KID_MISMATCH,
201
+ "request_kid does not match canonical hash of params",
202
+ )
203
+ sig_input = _canon(
204
+ [
205
+ "MCP/Provenance/v1",
206
+ method,
207
+ params_without_provenance,
208
+ provenance.calling_agent_kid,
209
+ provenance.cap_token_root,
210
+ provenance.request_kid,
211
+ ]
212
+ )
213
+ if not verify_ed25519(provenance.calling_agent_kid, provenance.signature, sig_input):
214
+ raise ProvenanceError(ERR_PROVENANCE_INVALID, "provenance signature invalid")
@@ -0,0 +1,152 @@
1
+ """RelataDB exposed to HyperMind agents as a research tool source (integration P2).
2
+
3
+ Registers a curated set of Relata knowledge/memory tools and dispatches calls to
4
+ Relata's MCP server via ``relata.McpClient``. This is the "agents research from
5
+ the tools they have access to" surface: an agent mid-``consult`` / deliberation
6
+ can recall memory, search the knowledge base, run a governed query, or traverse
7
+ the entity graph — all sourced from Relata, all tenant-scoped.
8
+
9
+ Two MCP layers, do not confuse them:
10
+ * HyperMind's own MCP (``src/hypermind/mcp/``) — agents expose/consume tools
11
+ per the HM protocol.
12
+ * This module — Relata offered as *one tool source* inside HM. They compose.
13
+
14
+ **Access is CapToken-gated.** Pass an agent's ``cap_token`` to :meth:`call` and
15
+ each dispatch is authorized against a ``Caveat.tool(...)`` scope (glob-subset
16
+ match), so you can grant an agent ``relata.recall`` without ``relata.query_knowledge``.
17
+
18
+ The ``relata`` SDK is imported lazily inside :meth:`open`; tests inject
19
+ ``mcp_factory``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Callable
25
+ from typing import Any
26
+
27
+ if False: # TYPE_CHECKING — avoid importing capability at module load
28
+ from hypermind.capability import CapToken
29
+
30
+ _PURPOSE = "agent_research"
31
+
32
+ # slug → Relata MCP tool name + MCP tools/list descriptor.
33
+ _RELATA_TOOLS: dict[str, dict[str, Any]] = {
34
+ "relata.recall": {
35
+ "mcp": "recall",
36
+ "description": "Recall memories relevant to a query, ranked by relevance/recency.",
37
+ "schema": {
38
+ "type": "object",
39
+ "properties": {"query": {"type": "string"}, "top_k": {"type": "integer"}},
40
+ "required": ["query"],
41
+ },
42
+ },
43
+ "relata.search_knowledge": {
44
+ "mcp": "search_knowledge",
45
+ "description": "Semantic + keyword search over the Relata knowledge base.",
46
+ "schema": {
47
+ "type": "object",
48
+ "properties": {"query": {"type": "string"}},
49
+ "required": ["query"],
50
+ },
51
+ },
52
+ "relata.query_knowledge": {
53
+ "mcp": "query_knowledge",
54
+ "description": "Run a governed, read-only SQL query against Relata.",
55
+ "schema": {
56
+ "type": "object",
57
+ "properties": {"sql": {"type": "string"}},
58
+ "required": ["sql"],
59
+ },
60
+ },
61
+ "relata.find_connections": {
62
+ "mcp": "find_connections",
63
+ "description": "Graph traversal: find entities connected to the given entity.",
64
+ "schema": {
65
+ "type": "object",
66
+ "properties": {"entity": {"type": "string"}, "limit": {"type": "integer"}},
67
+ "required": ["entity"],
68
+ },
69
+ },
70
+ }
71
+
72
+ McpFactory = Callable[[], Any]
73
+
74
+
75
+ class RelataToolProvider:
76
+ """Adapts Relata's MCP tools into HyperMind's tool surface. One per (server, namespace)."""
77
+
78
+ def __init__(
79
+ self,
80
+ base_url: str,
81
+ *,
82
+ namespace_id: str,
83
+ bearer_token: str | None = None,
84
+ purpose: str = _PURPOSE,
85
+ mcp_factory: McpFactory | None = None,
86
+ ) -> None:
87
+ self._url = base_url
88
+ self._ns = namespace_id
89
+ self._tok = bearer_token
90
+ self._purpose = purpose
91
+ self._factory = mcp_factory
92
+ self._mcp: Any = None
93
+
94
+ def open(self) -> None:
95
+ if self._mcp is not None:
96
+ return
97
+ if self._factory is not None:
98
+ self._mcp = self._factory()
99
+ else: # pragma: no cover - requires the optional `relata` SDK + live server
100
+ from relata import RelataClient
101
+ from relata.mcp import McpClient
102
+
103
+ client = RelataClient(
104
+ self._url,
105
+ bearer_token=self._tok,
106
+ purpose=self._purpose,
107
+ tenant=self._ns, # tenant isolation
108
+ )
109
+ self._mcp = McpClient.from_client(client)
110
+
111
+ def close(self) -> None:
112
+ if self._mcp is not None:
113
+ self._mcp.close()
114
+ self._mcp = None
115
+
116
+ def _require(self) -> Any:
117
+ if self._mcp is None:
118
+ raise RuntimeError("RelataToolProvider used before open()")
119
+ return self._mcp
120
+
121
+ def tool_defs(self) -> list[dict[str, Any]]:
122
+ """MCP ``tools/list``-shaped descriptors — mergeable into HM's tool view."""
123
+ return [
124
+ {"name": slug, "description": spec["description"], "inputSchema": spec["schema"]}
125
+ for slug, spec in _RELATA_TOOLS.items()
126
+ ]
127
+
128
+ def call(
129
+ self,
130
+ slug: str,
131
+ arguments: dict[str, Any],
132
+ *,
133
+ cap_token: CapToken | None = None,
134
+ **authorize_kwargs: Any,
135
+ ) -> dict[str, Any]:
136
+ """Dispatch a whitelisted Relata tool call.
137
+
138
+ When ``cap_token`` is provided, the call is authorized against a
139
+ ``Caveat.tool(slug)`` scope first (raising ``AuthError`` if not granted).
140
+ Extra keyword args flow to :meth:`CapToken.authorize` (e.g.
141
+ ``expected_root_issuer=``, ``revocation_store=``). Delegated tokens must
142
+ be chain-verified by the caller before use.
143
+ """
144
+ spec = _RELATA_TOOLS.get(slug)
145
+ if spec is None:
146
+ raise KeyError(f"unknown Relata tool {slug!r}; known: {sorted(_RELATA_TOOLS)}")
147
+ if cap_token is not None:
148
+ cap_token.authorize(action="tool_call", tool=slug, **authorize_kwargs)
149
+ # Relata's MCP tools expect a governance `purpose`; inject ours unless
150
+ # the caller overrides (mirrors relata.McpClient's typed methods).
151
+ args = {"purpose": self._purpose, **arguments}
152
+ return dict(self._require().call_tool(spec["mcp"], args))