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,71 @@
|
|
|
1
|
+
"""StorageBackend Protocol — the contract every persistence layer satisfies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from hypermind.dispute import Dispute
|
|
9
|
+
from hypermind.wire import Kid, SignedStatement
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StorageBackend(Protocol):
|
|
13
|
+
"""Durable storage for a HyperMind world.
|
|
14
|
+
|
|
15
|
+
Every method is synchronous — backends should use threadlocal
|
|
16
|
+
connections and short transactions. Async wrapping happens at the
|
|
17
|
+
caller (``_NamespaceWorld``) when needed.
|
|
18
|
+
|
|
19
|
+
Implementations MUST be safe to call from multiple coroutines on a
|
|
20
|
+
single event loop. They are NOT required to be safe under multiple
|
|
21
|
+
OS threads, multiple processes, or concurrent writers.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def open(self) -> None:
|
|
25
|
+
"""Connect, create schema if missing, prepare for I/O."""
|
|
26
|
+
|
|
27
|
+
def close(self) -> None:
|
|
28
|
+
"""Flush and disconnect. Idempotent."""
|
|
29
|
+
|
|
30
|
+
# ---- Claims ---------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
def append_claim(self, stmt: SignedStatement) -> None:
|
|
33
|
+
"""Persist one sealed claim. Idempotent on ``stmt.kid``."""
|
|
34
|
+
|
|
35
|
+
def load_claims(self) -> list[SignedStatement]:
|
|
36
|
+
"""Return every persisted claim in append order."""
|
|
37
|
+
|
|
38
|
+
# ---- Disputes -------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def append_dispute(self, kid: Kid, dispute: Dispute) -> None:
|
|
41
|
+
"""Persist (or upsert) a dispute keyed by claim ``kid``."""
|
|
42
|
+
|
|
43
|
+
def load_disputes(self) -> dict[Kid, Dispute]:
|
|
44
|
+
"""Return every persisted dispute as ``{claim_kid: Dispute}``."""
|
|
45
|
+
|
|
46
|
+
# ---- Reputation -----------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def snapshot_reputation(self, rows: list[tuple[bytes, str, float, float, int]]) -> None:
|
|
49
|
+
"""Persist reputation rows ``(agent_kid, topic, alpha, beta, last_update_ms)``."""
|
|
50
|
+
|
|
51
|
+
def load_reputation(self) -> list[tuple[bytes, str, float, float, int]]:
|
|
52
|
+
"""Return every persisted reputation row."""
|
|
53
|
+
|
|
54
|
+
# ---- Merkle leaves --------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def append_merkle_leaf(self, leaf: bytes) -> int:
|
|
57
|
+
"""Append a Merkle leaf; return its 1-based index in the log."""
|
|
58
|
+
|
|
59
|
+
def load_merkle_leaves(self) -> list[bytes]:
|
|
60
|
+
"""Return every persisted Merkle leaf in append order."""
|
|
61
|
+
|
|
62
|
+
# ---- FROST transcripts (DURABILITY for nonce-reuse defence) --------
|
|
63
|
+
|
|
64
|
+
def remember_transcript(self, transcript_id: bytes) -> bool:
|
|
65
|
+
"""Record a FROST transcript_id. Return True if newly added,
|
|
66
|
+
False if the transcript was already present (nonce reuse)."""
|
|
67
|
+
|
|
68
|
+
# ---- Health ---------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def stats(self) -> dict[str, int]:
|
|
71
|
+
"""Return basic metrics: ``{claims, disputes, reputation_rows, leaves, transcripts}``."""
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""RelataDB-backed StorageBackend — durable ledger over the Relata Python SDK.
|
|
2
|
+
|
|
3
|
+
Same :class:`~hypermind.storage.backend.StorageBackend` Protocol as
|
|
4
|
+
:class:`~hypermind.storage.sqlite.SqliteBackend`; swaps single-file SQLite for a
|
|
5
|
+
governed, bi-temporal, multi-tenant RelataDB server reached over HTTP / pgwire.
|
|
6
|
+
|
|
7
|
+
Identity mapping (the spine): **HyperMind namespace → Relata tenant**. Every
|
|
8
|
+
sub-client carries ``X-Organization-Id = namespace_id`` so claims, disputes,
|
|
9
|
+
reputation, and the Merkle log are tenant-isolated. Within the tenant:
|
|
10
|
+
|
|
11
|
+
* claim ``kid`` → ``object_id`` of ``HmClaim`` (wire bytes, base64)
|
|
12
|
+
* dispute (claim ``kid``)→ ``object_id`` of ``HmDispute`` (pickled, base64)
|
|
13
|
+
* (agent, topic) → ``object_id`` of ``HmReputation``
|
|
14
|
+
* Merkle leaf → Relata's durable WAL-backed ordered log (``LogClient``)
|
|
15
|
+
* FROST transcript id → ``object_id`` of ``HmTranscript`` (durable test-and-set)
|
|
16
|
+
|
|
17
|
+
**Why transcripts use an object row, not ``TokenClient``:** Relata's dedup-token
|
|
18
|
+
store is in-memory today (resets on restart), which fails the replay-defence
|
|
19
|
+
durability contract. The ``HmTranscript`` object row is WAL-durable and safe
|
|
20
|
+
under the single-writer-per-namespace model.
|
|
21
|
+
|
|
22
|
+
The ``relata`` SDK is imported lazily inside :meth:`open` so this module imports
|
|
23
|
+
cleanly without the optional ``hypermind[relata]`` extra installed. Tests inject
|
|
24
|
+
a ``client_factory`` to run the full round-trip against a fake SDK — no live
|
|
25
|
+
server required.
|
|
26
|
+
|
|
27
|
+
Run the server on ``RELATA_ORG_MODE=strict`` so type-ownership isolation is
|
|
28
|
+
enforced (row-level ``with_tenant`` filtering applies regardless of mode).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import base64
|
|
34
|
+
import pickle
|
|
35
|
+
import time
|
|
36
|
+
from collections.abc import Callable
|
|
37
|
+
from typing import TYPE_CHECKING, Any
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from hypermind.dispute import Dispute
|
|
41
|
+
from hypermind.wire import Kid, SignedStatement
|
|
42
|
+
|
|
43
|
+
_PURPOSE = "hm_ledger"
|
|
44
|
+
|
|
45
|
+
# A factory returns the three clients the backend needs, already tenant-scoped:
|
|
46
|
+
# (query_client, log_client, object_client)
|
|
47
|
+
# ``query_client`` needs ``.query(sql) -> QueryResult`` (with ``.rows``) + ``.close()``.
|
|
48
|
+
ClientFactory = Callable[[], "tuple[Any, Any, Any]"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RelataBackend:
|
|
52
|
+
"""RelataDB StorageBackend. One instance per (server, namespace)."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
base_url: str,
|
|
57
|
+
*,
|
|
58
|
+
namespace_id: str,
|
|
59
|
+
bearer_token: str | None = None,
|
|
60
|
+
purpose: str = _PURPOSE,
|
|
61
|
+
client_factory: ClientFactory | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self._url = base_url
|
|
64
|
+
self._ns = namespace_id
|
|
65
|
+
self._tok = bearer_token
|
|
66
|
+
self._purpose = purpose
|
|
67
|
+
self._factory = client_factory
|
|
68
|
+
self._c: Any = None
|
|
69
|
+
self._log: Any = None
|
|
70
|
+
self._obj: Any = None
|
|
71
|
+
self._seq = 1
|
|
72
|
+
# ponytail: in-memory set gives read-your-writes consistency for the test-and-set;
|
|
73
|
+
# the durable upsert covers the restart-replay defence.
|
|
74
|
+
self._seen_transcripts: set[str] = set()
|
|
75
|
+
|
|
76
|
+
# ---- Lifecycle ------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def open(self) -> None:
|
|
79
|
+
if self._c is not None:
|
|
80
|
+
return
|
|
81
|
+
if self._factory is not None:
|
|
82
|
+
self._c, self._log, self._obj = self._factory()
|
|
83
|
+
else: # pragma: no cover - requires the optional `relata` SDK + live server
|
|
84
|
+
from relata import RelataClient
|
|
85
|
+
from relata.log import LogClient
|
|
86
|
+
from relata.objects import ObjectClient
|
|
87
|
+
|
|
88
|
+
self._c = RelataClient(
|
|
89
|
+
self._url,
|
|
90
|
+
bearer_token=self._tok,
|
|
91
|
+
purpose=self._purpose,
|
|
92
|
+
tenant=self._ns, # → X-Organization-Id on every request
|
|
93
|
+
)
|
|
94
|
+
# from_client forwards _extra_headers (incl. the tenant header) so
|
|
95
|
+
# the log + object doors stay tenant-scoped. Do NOT construct these
|
|
96
|
+
# bare — that would un-tenant the Merkle log.
|
|
97
|
+
self._log = LogClient.from_client(self._c)
|
|
98
|
+
self._obj = ObjectClient.from_client(self._c)
|
|
99
|
+
# Initialise next_seq from the high-water mark so append order survives
|
|
100
|
+
# restart. Relata's SQL subset rejects COALESCE(MAX(...)); use ORDER BY
|
|
101
|
+
# DESC LIMIT 1 instead (verified against the live server).
|
|
102
|
+
rows = self._rows("SELECT seq FROM HmClaim ORDER BY seq DESC LIMIT 1")
|
|
103
|
+
self._seq = (int(rows[0]["seq"]) + 1) if rows else 1
|
|
104
|
+
|
|
105
|
+
def close(self) -> None:
|
|
106
|
+
for client in (self._log, self._obj, self._c):
|
|
107
|
+
if client is not None:
|
|
108
|
+
client.close()
|
|
109
|
+
self._c = self._log = self._obj = None
|
|
110
|
+
|
|
111
|
+
def _require(self) -> Any:
|
|
112
|
+
if self._c is None:
|
|
113
|
+
raise RuntimeError("RelataBackend used before open()")
|
|
114
|
+
return self._c
|
|
115
|
+
|
|
116
|
+
def _rows(self, sql: str) -> list[dict[str, Any]]:
|
|
117
|
+
return list(self._require().query(sql).rows)
|
|
118
|
+
|
|
119
|
+
# ---- Claims (immutable, append-ordered) -----------------------------
|
|
120
|
+
|
|
121
|
+
def append_claim(self, stmt: SignedStatement) -> None:
|
|
122
|
+
self._obj.upsert(
|
|
123
|
+
"HmClaim",
|
|
124
|
+
bytes(stmt.kid).hex(),
|
|
125
|
+
{
|
|
126
|
+
"seq": self._seq,
|
|
127
|
+
"wire_b64": base64.b64encode(stmt.to_bytes()).decode("ascii"),
|
|
128
|
+
"created_ms": int(time.time() * 1000),
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
self._seq += 1
|
|
132
|
+
|
|
133
|
+
def load_claims(self) -> list[SignedStatement]:
|
|
134
|
+
from hypermind.wire import SignedStatement
|
|
135
|
+
|
|
136
|
+
rows = self._rows("SELECT wire_b64, seq FROM HmClaim ORDER BY seq ASC")
|
|
137
|
+
return [SignedStatement.from_bytes(base64.b64decode(r["wire_b64"])) for r in rows]
|
|
138
|
+
|
|
139
|
+
# ---- Disputes (upsert by claim kid) ---------------------------------
|
|
140
|
+
|
|
141
|
+
def append_dispute(self, kid: Kid, dispute: Dispute) -> None:
|
|
142
|
+
payload = pickle.dumps(dispute, protocol=pickle.HIGHEST_PROTOCOL)
|
|
143
|
+
self._obj.upsert(
|
|
144
|
+
"HmDispute",
|
|
145
|
+
bytes(kid).hex(),
|
|
146
|
+
{
|
|
147
|
+
"claim_kid": bytes(kid).hex(),
|
|
148
|
+
"payload_b64": base64.b64encode(payload).decode("ascii"),
|
|
149
|
+
"updated_ms": int(time.time() * 1000),
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def load_disputes(self) -> dict[Kid, Dispute]:
|
|
154
|
+
out: dict[bytes, Dispute] = {}
|
|
155
|
+
for r in self._rows("SELECT claim_kid, payload_b64 FROM HmDispute"):
|
|
156
|
+
try:
|
|
157
|
+
out[bytes.fromhex(r["claim_kid"])] = pickle.loads(
|
|
158
|
+
base64.b64decode(r["payload_b64"])
|
|
159
|
+
)
|
|
160
|
+
except Exception:
|
|
161
|
+
continue
|
|
162
|
+
return out
|
|
163
|
+
|
|
164
|
+
# ---- Reputation (bulk upsert, keyed on agent+topic) -----------------
|
|
165
|
+
|
|
166
|
+
def snapshot_reputation(self, rows: list[tuple[bytes, str, float, float, int]]) -> None:
|
|
167
|
+
if not rows:
|
|
168
|
+
return
|
|
169
|
+
self._obj.batch_upsert(
|
|
170
|
+
"HmReputation",
|
|
171
|
+
[
|
|
172
|
+
{
|
|
173
|
+
"id": f"{agent.hex()}:{topic}",
|
|
174
|
+
"agent_hex": agent.hex(),
|
|
175
|
+
"topic": topic,
|
|
176
|
+
"alpha": alpha,
|
|
177
|
+
"beta": beta,
|
|
178
|
+
"last_update_ms": last_ms,
|
|
179
|
+
}
|
|
180
|
+
for (agent, topic, alpha, beta, last_ms) in rows
|
|
181
|
+
],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def load_reputation(self) -> list[tuple[bytes, str, float, float, int]]:
|
|
185
|
+
rows = self._rows("SELECT agent_hex, topic, alpha, beta, last_update_ms FROM HmReputation")
|
|
186
|
+
return [
|
|
187
|
+
(bytes.fromhex(r["agent_hex"]), r["topic"], r["alpha"], r["beta"], r["last_update_ms"])
|
|
188
|
+
for r in rows
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
# ---- Merkle leaves → Relata's durable ordered log -------------------
|
|
192
|
+
|
|
193
|
+
def append_merkle_leaf(self, leaf: bytes) -> int:
|
|
194
|
+
index, _commit_ns = self._log.append(leaf)
|
|
195
|
+
return int(index)
|
|
196
|
+
|
|
197
|
+
def load_merkle_leaves(self) -> list[bytes]:
|
|
198
|
+
# `since` semantics vary (SDK docstring says inclusive; the live server
|
|
199
|
+
# behaves exclusive), so page defensively: dedup by index, advance to the
|
|
200
|
+
# max index seen, and stop when a page yields no new leaves. Correct
|
|
201
|
+
# under either semantics. Ordered by index at the end.
|
|
202
|
+
seen: dict[int, bytes] = {}
|
|
203
|
+
since = 0
|
|
204
|
+
while True:
|
|
205
|
+
batch = self._log.load_leaves(since=since, limit=1000)
|
|
206
|
+
new = [lf for lf in batch if lf.index not in seen]
|
|
207
|
+
if not new:
|
|
208
|
+
break
|
|
209
|
+
for lf in new:
|
|
210
|
+
seen[lf.index] = lf.data
|
|
211
|
+
since = max(lf.index for lf in batch)
|
|
212
|
+
return [seen[i] for i in sorted(seen)]
|
|
213
|
+
|
|
214
|
+
# ---- FROST transcripts (durable test-and-set) -----------------------
|
|
215
|
+
|
|
216
|
+
def remember_transcript(self, transcript_id: bytes) -> bool:
|
|
217
|
+
"""Return True if newly recorded, False on duplicate.
|
|
218
|
+
|
|
219
|
+
Single-writer-per-namespace check-then-insert. ``tid`` is hex (0-9a-f)
|
|
220
|
+
so the WHERE literal is injection-safe.
|
|
221
|
+
"""
|
|
222
|
+
tid = transcript_id.hex()
|
|
223
|
+
if tid in self._seen_transcripts:
|
|
224
|
+
return False
|
|
225
|
+
# Also check durable store to catch replays across restarts.
|
|
226
|
+
if self._rows(f"SELECT id FROM HmTranscript WHERE id = '{tid}' LIMIT 1"):
|
|
227
|
+
self._seen_transcripts.add(tid)
|
|
228
|
+
return False
|
|
229
|
+
self._obj.upsert("HmTranscript", tid, {"created_ms": int(time.time() * 1000)})
|
|
230
|
+
self._seen_transcripts.add(tid)
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
# ---- Health ---------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def stats(self) -> dict[str, int]:
|
|
236
|
+
def count(table: str) -> int:
|
|
237
|
+
return int(self._rows(f"SELECT COUNT(*) AS n FROM {table}")[0]["n"])
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"claims": count("HmClaim"),
|
|
241
|
+
"disputes": count("HmDispute"),
|
|
242
|
+
"reputation_rows": count("HmReputation"),
|
|
243
|
+
"leaves": int(self._log.head()),
|
|
244
|
+
"transcripts": count("HmTranscript"),
|
|
245
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""SQLite-backed StorageBackend — single-file durability, zero extra deps.
|
|
2
|
+
|
|
3
|
+
Schema (auto-created on ``open()``):
|
|
4
|
+
|
|
5
|
+
claims
|
|
6
|
+
kid BLOB PRIMARY KEY
|
|
7
|
+
seq INTEGER NOT NULL -- monotonic append order
|
|
8
|
+
bytes BLOB NOT NULL -- canonical CBOR-encoded SignedStatement
|
|
9
|
+
created_ms INTEGER NOT NULL
|
|
10
|
+
|
|
11
|
+
disputes
|
|
12
|
+
claim_kid BLOB PRIMARY KEY -- the disputed claim kid
|
|
13
|
+
payload BLOB NOT NULL -- pickled Dispute (bond+state+transitions)
|
|
14
|
+
updated_ms INTEGER NOT NULL
|
|
15
|
+
|
|
16
|
+
reputation
|
|
17
|
+
agent_kid BLOB NOT NULL
|
|
18
|
+
topic TEXT NOT NULL
|
|
19
|
+
alpha REAL NOT NULL
|
|
20
|
+
beta REAL NOT NULL
|
|
21
|
+
last_update_ms INTEGER NOT NULL
|
|
22
|
+
PRIMARY KEY (agent_kid, topic)
|
|
23
|
+
|
|
24
|
+
merkle_leaves
|
|
25
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT
|
|
26
|
+
leaf BLOB NOT NULL
|
|
27
|
+
|
|
28
|
+
frost_transcripts
|
|
29
|
+
transcript_id BLOB PRIMARY KEY
|
|
30
|
+
created_ms INTEGER NOT NULL
|
|
31
|
+
|
|
32
|
+
The ``Dispute`` payload is pickled rather than columnised because the
|
|
33
|
+
state-transition history is variable-length and the field set will grow
|
|
34
|
+
across versions. Pickle is acceptable here — the SQLite file is local-
|
|
35
|
+
process-private; no untrusted dispute payloads cross this boundary.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import pickle
|
|
41
|
+
import sqlite3
|
|
42
|
+
import threading
|
|
43
|
+
import time
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import TYPE_CHECKING
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from hypermind.dispute import Dispute
|
|
49
|
+
from hypermind.wire import Kid, SignedStatement
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_SCHEMA = """
|
|
53
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
54
|
+
kid BLOB PRIMARY KEY,
|
|
55
|
+
seq INTEGER NOT NULL,
|
|
56
|
+
bytes BLOB NOT NULL,
|
|
57
|
+
created_ms INTEGER NOT NULL
|
|
58
|
+
);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_claims_seq ON claims(seq);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS disputes (
|
|
62
|
+
claim_kid BLOB PRIMARY KEY,
|
|
63
|
+
payload BLOB NOT NULL,
|
|
64
|
+
updated_ms INTEGER NOT NULL
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS reputation (
|
|
68
|
+
agent_kid BLOB NOT NULL,
|
|
69
|
+
topic TEXT NOT NULL,
|
|
70
|
+
alpha REAL NOT NULL,
|
|
71
|
+
beta REAL NOT NULL,
|
|
72
|
+
last_update_ms INTEGER NOT NULL,
|
|
73
|
+
PRIMARY KEY (agent_kid, topic)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS merkle_leaves (
|
|
77
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
leaf BLOB NOT NULL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS frost_transcripts (
|
|
82
|
+
transcript_id BLOB PRIMARY KEY,
|
|
83
|
+
created_ms INTEGER NOT NULL
|
|
84
|
+
);
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SqliteBackend:
|
|
89
|
+
"""File-backed StorageBackend.
|
|
90
|
+
|
|
91
|
+
Construct with a path; pass ``":memory:"`` for an ephemeral test backend
|
|
92
|
+
(useful when you want the persistence code paths exercised without
|
|
93
|
+
touching disk).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, path: str | Path) -> None:
|
|
97
|
+
self.path = str(path)
|
|
98
|
+
self._conn: sqlite3.Connection | None = None
|
|
99
|
+
self._lock = threading.Lock()
|
|
100
|
+
self._next_seq = 1
|
|
101
|
+
|
|
102
|
+
# ---- Lifecycle ------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def open(self) -> None:
|
|
105
|
+
if self._conn is not None:
|
|
106
|
+
return
|
|
107
|
+
# ``check_same_thread=False`` because asyncio may schedule writes
|
|
108
|
+
# from different worker threads under uvicorn — we serialise via
|
|
109
|
+
# ``self._lock`` instead.
|
|
110
|
+
self._conn = sqlite3.connect(
|
|
111
|
+
self.path,
|
|
112
|
+
check_same_thread=False,
|
|
113
|
+
isolation_level=None, # autocommit; we manage transactions explicitly
|
|
114
|
+
)
|
|
115
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
116
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
117
|
+
self._conn.executescript(_SCHEMA)
|
|
118
|
+
# Initialise next_seq from the high-water mark so claim ordering
|
|
119
|
+
# survives restart.
|
|
120
|
+
row = self._conn.execute("SELECT COALESCE(MAX(seq), 0) FROM claims").fetchone()
|
|
121
|
+
self._next_seq = (row[0] or 0) + 1
|
|
122
|
+
|
|
123
|
+
def close(self) -> None:
|
|
124
|
+
with self._lock:
|
|
125
|
+
if self._conn is not None:
|
|
126
|
+
self._conn.close()
|
|
127
|
+
self._conn = None
|
|
128
|
+
|
|
129
|
+
def _require(self) -> sqlite3.Connection:
|
|
130
|
+
if self._conn is None:
|
|
131
|
+
raise RuntimeError("SqliteBackend used before open()")
|
|
132
|
+
return self._conn
|
|
133
|
+
|
|
134
|
+
# ---- Claims ---------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def append_claim(self, stmt: SignedStatement) -> None:
|
|
137
|
+
kid = bytes(stmt.kid)
|
|
138
|
+
wire = stmt.to_bytes()
|
|
139
|
+
now = int(time.time() * 1000)
|
|
140
|
+
with self._lock:
|
|
141
|
+
conn = self._require()
|
|
142
|
+
conn.execute(
|
|
143
|
+
"INSERT OR IGNORE INTO claims (kid, seq, bytes, created_ms) VALUES (?, ?, ?, ?)",
|
|
144
|
+
(kid, self._next_seq, wire, now),
|
|
145
|
+
)
|
|
146
|
+
self._next_seq += 1
|
|
147
|
+
|
|
148
|
+
def load_claims(self) -> list[SignedStatement]:
|
|
149
|
+
from hypermind.wire import SignedStatement
|
|
150
|
+
|
|
151
|
+
with self._lock:
|
|
152
|
+
conn = self._require()
|
|
153
|
+
rows = conn.execute("SELECT bytes FROM claims ORDER BY seq ASC").fetchall()
|
|
154
|
+
return [SignedStatement.from_bytes(bytes(r[0])) for r in rows]
|
|
155
|
+
|
|
156
|
+
# ---- Disputes -------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def append_dispute(self, kid: Kid, dispute: Dispute) -> None:
|
|
159
|
+
payload = pickle.dumps(dispute, protocol=pickle.HIGHEST_PROTOCOL)
|
|
160
|
+
now = int(time.time() * 1000)
|
|
161
|
+
with self._lock:
|
|
162
|
+
conn = self._require()
|
|
163
|
+
conn.execute(
|
|
164
|
+
"INSERT INTO disputes (claim_kid, payload, updated_ms) "
|
|
165
|
+
"VALUES (?, ?, ?) "
|
|
166
|
+
"ON CONFLICT(claim_kid) DO UPDATE SET "
|
|
167
|
+
"payload=excluded.payload, updated_ms=excluded.updated_ms",
|
|
168
|
+
(bytes(kid), payload, now),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def load_disputes(self) -> dict[Kid, Dispute]:
|
|
172
|
+
with self._lock:
|
|
173
|
+
conn = self._require()
|
|
174
|
+
rows = conn.execute("SELECT claim_kid, payload FROM disputes").fetchall()
|
|
175
|
+
out: dict[bytes, Dispute] = {}
|
|
176
|
+
for kid, payload in rows:
|
|
177
|
+
try:
|
|
178
|
+
out[bytes(kid)] = pickle.loads(bytes(payload))
|
|
179
|
+
except Exception:
|
|
180
|
+
continue
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
# ---- Reputation -----------------------------------------------------
|
|
184
|
+
|
|
185
|
+
def snapshot_reputation(self, rows: list[tuple[bytes, str, float, float, int]]) -> None:
|
|
186
|
+
if not rows:
|
|
187
|
+
return
|
|
188
|
+
with self._lock:
|
|
189
|
+
conn = self._require()
|
|
190
|
+
conn.execute("BEGIN")
|
|
191
|
+
try:
|
|
192
|
+
conn.executemany(
|
|
193
|
+
"INSERT INTO reputation "
|
|
194
|
+
"(agent_kid, topic, alpha, beta, last_update_ms) "
|
|
195
|
+
"VALUES (?, ?, ?, ?, ?) "
|
|
196
|
+
"ON CONFLICT(agent_kid, topic) DO UPDATE SET "
|
|
197
|
+
"alpha=excluded.alpha, beta=excluded.beta, "
|
|
198
|
+
"last_update_ms=excluded.last_update_ms",
|
|
199
|
+
rows,
|
|
200
|
+
)
|
|
201
|
+
conn.execute("COMMIT")
|
|
202
|
+
except Exception:
|
|
203
|
+
conn.execute("ROLLBACK")
|
|
204
|
+
raise
|
|
205
|
+
|
|
206
|
+
def load_reputation(self) -> list[tuple[bytes, str, float, float, int]]:
|
|
207
|
+
with self._lock:
|
|
208
|
+
conn = self._require()
|
|
209
|
+
rows = conn.execute(
|
|
210
|
+
"SELECT agent_kid, topic, alpha, beta, last_update_ms FROM reputation"
|
|
211
|
+
).fetchall()
|
|
212
|
+
return [(bytes(r[0]), r[1], r[2], r[3], r[4]) for r in rows]
|
|
213
|
+
|
|
214
|
+
# ---- Merkle leaves --------------------------------------------------
|
|
215
|
+
|
|
216
|
+
def append_merkle_leaf(self, leaf: bytes) -> int:
|
|
217
|
+
with self._lock:
|
|
218
|
+
conn = self._require()
|
|
219
|
+
cur = conn.execute("INSERT INTO merkle_leaves (leaf) VALUES (?)", (leaf,))
|
|
220
|
+
return int(cur.lastrowid or 0)
|
|
221
|
+
|
|
222
|
+
def load_merkle_leaves(self) -> list[bytes]:
|
|
223
|
+
with self._lock:
|
|
224
|
+
conn = self._require()
|
|
225
|
+
rows = conn.execute("SELECT leaf FROM merkle_leaves ORDER BY seq ASC").fetchall()
|
|
226
|
+
return [bytes(r[0]) for r in rows]
|
|
227
|
+
|
|
228
|
+
# ---- FROST transcripts ---------------------------------------------
|
|
229
|
+
|
|
230
|
+
def remember_transcript(self, transcript_id: bytes) -> bool:
|
|
231
|
+
"""Atomic insert. Returns True if newly recorded, False on duplicate.
|
|
232
|
+
|
|
233
|
+
Uses ``INSERT OR IGNORE`` so two callers racing on the same
|
|
234
|
+
transcript_id always have one survivor — the loser sees False
|
|
235
|
+
and MUST refuse to release its z_i (FROST nonce-reuse defence).
|
|
236
|
+
"""
|
|
237
|
+
now = int(time.time() * 1000)
|
|
238
|
+
with self._lock:
|
|
239
|
+
conn = self._require()
|
|
240
|
+
cur = conn.execute(
|
|
241
|
+
"INSERT OR IGNORE INTO frost_transcripts (transcript_id, created_ms) VALUES (?, ?)",
|
|
242
|
+
(transcript_id, now),
|
|
243
|
+
)
|
|
244
|
+
return cur.rowcount > 0
|
|
245
|
+
|
|
246
|
+
# ---- Health ---------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def stats(self) -> dict[str, int]:
|
|
249
|
+
with self._lock:
|
|
250
|
+
conn = self._require()
|
|
251
|
+
rows = {
|
|
252
|
+
"claims": conn.execute("SELECT COUNT(*) FROM claims").fetchone()[0],
|
|
253
|
+
"disputes": conn.execute("SELECT COUNT(*) FROM disputes").fetchone()[0],
|
|
254
|
+
"reputation_rows": conn.execute("SELECT COUNT(*) FROM reputation").fetchone()[0],
|
|
255
|
+
"leaves": conn.execute("SELECT COUNT(*) FROM merkle_leaves").fetchone()[0],
|
|
256
|
+
"transcripts": conn.execute("SELECT COUNT(*) FROM frost_transcripts").fetchone()[0],
|
|
257
|
+
}
|
|
258
|
+
return rows
|