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
hypermind/mcp/client.py
ADDED
|
@@ -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))
|