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,523 @@
|
|
|
1
|
+
"""SwarmMemory — CRDT-backed shared scratchpad for the collective.
|
|
2
|
+
|
|
3
|
+
Three sub-stores per spec §20.4:
|
|
4
|
+
|
|
5
|
+
* **consensus_state** — per-topic compressed posterior + last HLC.
|
|
6
|
+
* **lessons_learned** — append-only (claim_kid, oracle_outcome, rationale).
|
|
7
|
+
* **swarm_state_vector** — fixed-dim embedding summarising recent consensus.
|
|
8
|
+
|
|
9
|
+
CRDT semantics: last-write-wins-with-evidence. Every write carries the
|
|
10
|
+
issuer kid + HLC; reads are local. Conflicts resolve by HLC (then by
|
|
11
|
+
kid as a tiebreaker).
|
|
12
|
+
|
|
13
|
+
Wire-format-clean: when serialised for gossip, each record rides inside
|
|
14
|
+
a SEAL envelope as ``payload.swarm_record`` — no envelope-schema-hash
|
|
15
|
+
bump, no v1.0 freeze risk.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import logging
|
|
22
|
+
import warnings
|
|
23
|
+
from collections import deque
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
import cbor2
|
|
28
|
+
|
|
29
|
+
_log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
SWARM_RECORD_VERSION = 1
|
|
32
|
+
SWARM_VECTOR_MAX_DIM = 1024
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SwarmVectorOverflowWarning(UserWarning):
|
|
36
|
+
"""Emitted when topic count exceeds SWARM_VECTOR_MAX_DIM and extra topics are truncated."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# DoS gate: oversize CBOR payloads are rejected before parsing. Swarm
|
|
40
|
+
# records carry vectors up to SWARM_VECTOR_MAX_DIM floats; 1 MiB is a
|
|
41
|
+
# generous ceiling that still bounds parser-amplification attacks.
|
|
42
|
+
MAX_SWARM_RECORD_BYTES = 1024 * 1024
|
|
43
|
+
MAX_LESSONS = 10_000
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Records
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ConsensusEntry:
|
|
53
|
+
topic: str
|
|
54
|
+
posterior: float
|
|
55
|
+
n_contributors: int
|
|
56
|
+
last_hlc_ms: int
|
|
57
|
+
issuer_kid: bytes
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class LessonEntry:
|
|
62
|
+
claim_kid: bytes
|
|
63
|
+
topic: str
|
|
64
|
+
oracle_outcome: float
|
|
65
|
+
predicted_consensus: float
|
|
66
|
+
rationale: str
|
|
67
|
+
hlc_ms: int
|
|
68
|
+
issuer_kid: bytes
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class SwarmStateVector:
|
|
73
|
+
topic: str
|
|
74
|
+
vector: tuple[float, ...]
|
|
75
|
+
last_hlc_ms: int
|
|
76
|
+
issuer_kid: bytes
|
|
77
|
+
|
|
78
|
+
def __post_init__(self) -> None:
|
|
79
|
+
if len(self.vector) > SWARM_VECTOR_MAX_DIM:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"swarm_state_vector dim {len(self.vector)} > max {SWARM_VECTOR_MAX_DIM}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Memory
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SwarmMemory:
|
|
91
|
+
"""Per-namespace shared memory. In-process LWW CRDT for v0.9b.
|
|
92
|
+
|
|
93
|
+
T4-04 (cross-namespace federation): a SwarmMemory may :meth:`federate`
|
|
94
|
+
with a remote SwarmMemory through a named bridge_kid. Bridge-mediated
|
|
95
|
+
merges record ``provenance_namespace`` on every fork-log entry so
|
|
96
|
+
auditors can trace where a conflict originated.
|
|
97
|
+
|
|
98
|
+
Per the §20 invariant (also enforced by ``mind/federation.assert_never_merge``)
|
|
99
|
+
foreign reputation/state is NEVER silently merged into the home
|
|
100
|
+
kernel — it is exposed via :meth:`foreign_consensus` for display only.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, namespace_id: bytes | None = None) -> None:
|
|
104
|
+
self.namespace_id: bytes | None = bytes(namespace_id) if namespace_id else None
|
|
105
|
+
self._consensus: dict[str, ConsensusEntry] = {}
|
|
106
|
+
self._lessons: list[LessonEntry] = []
|
|
107
|
+
self._vectors: dict[str, SwarmStateVector] = {}
|
|
108
|
+
# Fork log: records (hlc, kid, winner_hash) for each conflict that
|
|
109
|
+
# was resolved by deterministic tiebreak rather than crashing.
|
|
110
|
+
# Bounded at 100 entries — older conflicts are evicted.
|
|
111
|
+
self.fork_log: deque[dict] = deque(maxlen=100)
|
|
112
|
+
# Cross-namespace bridges: remote_namespace_id_hex -> bridge_kid.
|
|
113
|
+
self._bridges: dict[str, bytes] = {}
|
|
114
|
+
# Foreign read-through cache populated on bridge merge; never
|
|
115
|
+
# writes into self._consensus to preserve never-merge invariant.
|
|
116
|
+
self._foreign_consensus: dict[tuple[str, str], ConsensusEntry] = {}
|
|
117
|
+
|
|
118
|
+
# ---- consensus_state ---------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def upsert_consensus(self, entry: ConsensusEntry) -> bool:
|
|
121
|
+
"""Apply LWW write. Returns True if accepted.
|
|
122
|
+
|
|
123
|
+
Tiebreak rules at identical HLC:
|
|
124
|
+
* smaller ``issuer_kid`` loses (lex compare, strict ``<``);
|
|
125
|
+
* equal ``issuer_kid`` requires byte-identical payload — a
|
|
126
|
+
divergent payload at the same (hlc, kid) is a CRDT protocol
|
|
127
|
+
violation and raises ``ValueError``. Same-(hlc, kid) with
|
|
128
|
+
identical payload is an idempotent no-op (returns True).
|
|
129
|
+
"""
|
|
130
|
+
existing = self._consensus.get(entry.topic)
|
|
131
|
+
if existing is not None:
|
|
132
|
+
if entry.last_hlc_ms < existing.last_hlc_ms:
|
|
133
|
+
return False
|
|
134
|
+
if entry.last_hlc_ms == existing.last_hlc_ms:
|
|
135
|
+
if entry.issuer_kid == existing.issuer_kid:
|
|
136
|
+
if entry == existing:
|
|
137
|
+
return True
|
|
138
|
+
# Conflict: same (hlc, kid) with different payload.
|
|
139
|
+
# Log the fork and apply deterministic tiebreak: keep
|
|
140
|
+
# the entry whose CBOR encoding has the lexicographically
|
|
141
|
+
# smaller SHA-256 hash (stable, reproducible on every peer).
|
|
142
|
+
import cbor2 as _cbor2
|
|
143
|
+
|
|
144
|
+
h_existing = hashlib.sha256(
|
|
145
|
+
_cbor2.dumps(
|
|
146
|
+
existing.issuer_kid
|
|
147
|
+
+ existing.last_hlc_ms.to_bytes(8, "big")
|
|
148
|
+
+ str(existing.posterior).encode(),
|
|
149
|
+
canonical=True,
|
|
150
|
+
)
|
|
151
|
+
).digest()
|
|
152
|
+
h_entry = hashlib.sha256(
|
|
153
|
+
_cbor2.dumps(
|
|
154
|
+
entry.issuer_kid
|
|
155
|
+
+ entry.last_hlc_ms.to_bytes(8, "big")
|
|
156
|
+
+ str(entry.posterior).encode(),
|
|
157
|
+
canonical=True,
|
|
158
|
+
)
|
|
159
|
+
).digest()
|
|
160
|
+
self.fork_log.append(
|
|
161
|
+
{
|
|
162
|
+
"store": "consensus",
|
|
163
|
+
"topic": entry.topic,
|
|
164
|
+
"hlc": entry.last_hlc_ms,
|
|
165
|
+
"kid": entry.issuer_kid,
|
|
166
|
+
"winner_hash": min(h_existing, h_entry).hex(),
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
_log.warning(
|
|
170
|
+
"SwarmMemory fork: consensus (hlc=%s, kid=%s) has conflicting payload; "
|
|
171
|
+
"applying deterministic tiebreak",
|
|
172
|
+
entry.last_hlc_ms,
|
|
173
|
+
entry.issuer_kid[:4].hex(),
|
|
174
|
+
)
|
|
175
|
+
if h_entry < h_existing:
|
|
176
|
+
self._consensus[entry.topic] = entry
|
|
177
|
+
return True
|
|
178
|
+
return False
|
|
179
|
+
if entry.issuer_kid < existing.issuer_kid:
|
|
180
|
+
return False
|
|
181
|
+
self._consensus[entry.topic] = entry
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def consensus(self, topic: str) -> ConsensusEntry | None:
|
|
185
|
+
return self._consensus.get(topic)
|
|
186
|
+
|
|
187
|
+
def all_consensus(self) -> list[ConsensusEntry]:
|
|
188
|
+
return sorted(self._consensus.values(), key=lambda c: c.topic)
|
|
189
|
+
|
|
190
|
+
# ---- lessons_learned ---------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def append_lesson(self, lesson: LessonEntry) -> None:
|
|
193
|
+
self._lessons.append(lesson)
|
|
194
|
+
if len(self._lessons) > MAX_LESSONS:
|
|
195
|
+
self._lessons = self._lessons[-MAX_LESSONS:]
|
|
196
|
+
|
|
197
|
+
def lessons_for(self, topic: str | None = None) -> list[LessonEntry]:
|
|
198
|
+
if topic is None:
|
|
199
|
+
return list(self._lessons)
|
|
200
|
+
return [lesson for lesson in self._lessons if lesson.topic == topic]
|
|
201
|
+
|
|
202
|
+
def lesson_count(self) -> int:
|
|
203
|
+
return len(self._lessons)
|
|
204
|
+
|
|
205
|
+
# ---- swarm_state_vector ------------------------------------------------
|
|
206
|
+
|
|
207
|
+
def upsert_vector(self, vec: SwarmStateVector) -> bool:
|
|
208
|
+
existing = self._vectors.get(vec.topic)
|
|
209
|
+
if existing is not None:
|
|
210
|
+
if vec.last_hlc_ms < existing.last_hlc_ms:
|
|
211
|
+
return False
|
|
212
|
+
if vec.last_hlc_ms == existing.last_hlc_ms:
|
|
213
|
+
if vec.issuer_kid == existing.issuer_kid:
|
|
214
|
+
if vec == existing:
|
|
215
|
+
return True
|
|
216
|
+
# Conflict: same (hlc, kid) with different payload.
|
|
217
|
+
import cbor2 as _cbor2
|
|
218
|
+
|
|
219
|
+
h_existing = hashlib.sha256(
|
|
220
|
+
_cbor2.dumps(
|
|
221
|
+
existing.issuer_kid
|
|
222
|
+
+ existing.last_hlc_ms.to_bytes(8, "big")
|
|
223
|
+
+ str(existing.vector).encode(),
|
|
224
|
+
canonical=True,
|
|
225
|
+
)
|
|
226
|
+
).digest()
|
|
227
|
+
h_vec = hashlib.sha256(
|
|
228
|
+
_cbor2.dumps(
|
|
229
|
+
vec.issuer_kid
|
|
230
|
+
+ vec.last_hlc_ms.to_bytes(8, "big")
|
|
231
|
+
+ str(vec.vector).encode(),
|
|
232
|
+
canonical=True,
|
|
233
|
+
)
|
|
234
|
+
).digest()
|
|
235
|
+
self.fork_log.append(
|
|
236
|
+
{
|
|
237
|
+
"store": "vectors",
|
|
238
|
+
"topic": vec.topic,
|
|
239
|
+
"hlc": vec.last_hlc_ms,
|
|
240
|
+
"kid": vec.issuer_kid,
|
|
241
|
+
"winner_hash": min(h_existing, h_vec).hex(),
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
_log.warning(
|
|
245
|
+
"SwarmMemory fork: vector (hlc=%s, kid=%s) has conflicting payload; "
|
|
246
|
+
"applying deterministic tiebreak",
|
|
247
|
+
vec.last_hlc_ms,
|
|
248
|
+
vec.issuer_kid[:4].hex(),
|
|
249
|
+
)
|
|
250
|
+
if h_vec < h_existing:
|
|
251
|
+
self._vectors[vec.topic] = vec
|
|
252
|
+
return True
|
|
253
|
+
return False
|
|
254
|
+
if vec.issuer_kid < existing.issuer_kid:
|
|
255
|
+
return False
|
|
256
|
+
self._vectors[vec.topic] = vec
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
def vector(self, topic: str) -> SwarmStateVector | None:
|
|
260
|
+
return self._vectors.get(topic)
|
|
261
|
+
|
|
262
|
+
# ---- bulk ops ----------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
# ---- T4-04: cross-namespace federation --------------------------------
|
|
265
|
+
|
|
266
|
+
def federate(self, remote_namespace_id: bytes, bridge_kid: bytes) -> None:
|
|
267
|
+
"""Establish a bridge to another namespace's SwarmMemory.
|
|
268
|
+
|
|
269
|
+
``bridge_kid`` is the kid of the bridge agent authorised to gossip
|
|
270
|
+
across the boundary. Only one bridge per remote namespace; calling
|
|
271
|
+
:meth:`federate` again replaces the existing bridge (rotation).
|
|
272
|
+
"""
|
|
273
|
+
ns_hex = bytes(remote_namespace_id).hex()
|
|
274
|
+
self._bridges[ns_hex] = bytes(bridge_kid)
|
|
275
|
+
|
|
276
|
+
def bridges(self) -> dict[str, bytes]:
|
|
277
|
+
return dict(self._bridges)
|
|
278
|
+
|
|
279
|
+
def merge_from_bridge(
|
|
280
|
+
self,
|
|
281
|
+
other: SwarmMemory,
|
|
282
|
+
*,
|
|
283
|
+
remote_namespace_id: bytes,
|
|
284
|
+
bridge_kid: bytes,
|
|
285
|
+
) -> dict[str, int]:
|
|
286
|
+
"""Bridge-mediated cross-namespace merge.
|
|
287
|
+
|
|
288
|
+
Foreign consensus entries are written into a *separate*
|
|
289
|
+
``_foreign_consensus`` map (read-only via :meth:`foreign_consensus`)
|
|
290
|
+
so the home reputation/state remains untouched. Conflicts produce
|
|
291
|
+
fork-log entries enriched with ``provenance_namespace``.
|
|
292
|
+
"""
|
|
293
|
+
ns_hex = bytes(remote_namespace_id).hex()
|
|
294
|
+
if self._bridges.get(ns_hex) != bytes(bridge_kid):
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"merge_from_bridge: bridge_kid does not match registered "
|
|
297
|
+
f"bridge for namespace {ns_hex[:16]}…"
|
|
298
|
+
)
|
|
299
|
+
applied = {"foreign_consensus": 0, "lessons": 0}
|
|
300
|
+
# Foreign consensus: never merged into home; always displayed.
|
|
301
|
+
for c in other.all_consensus():
|
|
302
|
+
key = (ns_hex, c.topic)
|
|
303
|
+
existing = self._foreign_consensus.get(key)
|
|
304
|
+
if existing is None or existing.last_hlc_ms < c.last_hlc_ms:
|
|
305
|
+
self._foreign_consensus[key] = c
|
|
306
|
+
applied["foreign_consensus"] += 1
|
|
307
|
+
elif existing.last_hlc_ms == c.last_hlc_ms and existing != c:
|
|
308
|
+
# Cross-namespace conflict at identical HLC — fork-log it.
|
|
309
|
+
self.fork_log.append(
|
|
310
|
+
{
|
|
311
|
+
"store": "foreign_consensus",
|
|
312
|
+
"topic": c.topic,
|
|
313
|
+
"hlc": c.last_hlc_ms,
|
|
314
|
+
"kid": c.issuer_kid,
|
|
315
|
+
"provenance_namespace": ns_hex,
|
|
316
|
+
"reason": "CROSS-NS-CONFLICT",
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
# Lessons may cross — they are append-only and globally addressed
|
|
320
|
+
# by claim_kid; no risk of silent reputation merge.
|
|
321
|
+
existing_kids = {lesson.claim_kid for lesson in self._lessons}
|
|
322
|
+
for lesson in other.lessons_for():
|
|
323
|
+
if lesson.claim_kid not in existing_kids:
|
|
324
|
+
self._lessons.append(lesson)
|
|
325
|
+
existing_kids.add(lesson.claim_kid)
|
|
326
|
+
applied["lessons"] += 1
|
|
327
|
+
return applied
|
|
328
|
+
|
|
329
|
+
def foreign_consensus(
|
|
330
|
+
self, *, remote_namespace_id: bytes | None = None, topic: str | None = None
|
|
331
|
+
) -> list[tuple[str, ConsensusEntry]]:
|
|
332
|
+
"""Return foreign consensus entries; ``[(ns_hex, entry), …]``.
|
|
333
|
+
|
|
334
|
+
Filtered by namespace and/or topic when provided. This is the
|
|
335
|
+
ONLY read path for foreign state — it is intentionally separate
|
|
336
|
+
from :meth:`consensus` to make the never-merge invariant
|
|
337
|
+
structural rather than aspirational.
|
|
338
|
+
"""
|
|
339
|
+
ns_filter = bytes(remote_namespace_id).hex() if remote_namespace_id else None
|
|
340
|
+
out: list[tuple[str, ConsensusEntry]] = []
|
|
341
|
+
for (ns_hex, tpc), entry in self._foreign_consensus.items():
|
|
342
|
+
if ns_filter is not None and ns_hex != ns_filter:
|
|
343
|
+
continue
|
|
344
|
+
if topic is not None and tpc != topic:
|
|
345
|
+
continue
|
|
346
|
+
out.append((ns_hex, entry))
|
|
347
|
+
return out
|
|
348
|
+
|
|
349
|
+
def merge(self, other: SwarmMemory) -> dict[str, int]:
|
|
350
|
+
"""LWW-merge another memory's records. Returns counters per sub-store.
|
|
351
|
+
|
|
352
|
+
Counts only *state-changing* applications — an idempotent
|
|
353
|
+
re-application of a byte-identical entry at the same (hlc, kid)
|
|
354
|
+
does not increment the counter.
|
|
355
|
+
"""
|
|
356
|
+
applied = {"consensus": 0, "lessons": 0, "vectors": 0}
|
|
357
|
+
for c in other.all_consensus():
|
|
358
|
+
before = self._consensus.get(c.topic)
|
|
359
|
+
if self.upsert_consensus(c) and self._consensus.get(c.topic) is not before:
|
|
360
|
+
applied["consensus"] += 1
|
|
361
|
+
# Lessons are append-only with idempotent dedup by claim_kid only.
|
|
362
|
+
# claim_kid is a 32-byte SHA-256 hash, globally unique per claim per spec,
|
|
363
|
+
# so deduping by claim_kid alone correctly drops re-gossip duplicates while
|
|
364
|
+
# still accepting valid lessons from different issuers on the same topic.
|
|
365
|
+
existing = {lesson.claim_kid for lesson in self._lessons}
|
|
366
|
+
for lesson in other.lessons_for():
|
|
367
|
+
if lesson.claim_kid not in existing:
|
|
368
|
+
self._lessons.append(lesson)
|
|
369
|
+
existing.add(lesson.claim_kid)
|
|
370
|
+
applied["lessons"] += 1
|
|
371
|
+
for v in other._vectors.values():
|
|
372
|
+
before_v = self._vectors.get(v.topic)
|
|
373
|
+
if self.upsert_vector(v) and self._vectors.get(v.topic) is not before_v:
|
|
374
|
+
applied["vectors"] += 1
|
|
375
|
+
return applied
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# Wire-clean encoding (rides inside SEAL payload)
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
_KEY_KIND = 0
|
|
383
|
+
_KEY_BODY = 1
|
|
384
|
+
_KEY_VERSION = 2
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def encode_swarm_record(
|
|
388
|
+
record: ConsensusEntry | LessonEntry | SwarmStateVector,
|
|
389
|
+
) -> bytes:
|
|
390
|
+
if isinstance(record, ConsensusEntry):
|
|
391
|
+
body = {
|
|
392
|
+
"topic": record.topic,
|
|
393
|
+
"posterior": record.posterior,
|
|
394
|
+
"n_contributors": record.n_contributors,
|
|
395
|
+
"last_hlc_ms": record.last_hlc_ms,
|
|
396
|
+
"issuer_kid": record.issuer_kid,
|
|
397
|
+
}
|
|
398
|
+
kind = "consensus"
|
|
399
|
+
elif isinstance(record, LessonEntry):
|
|
400
|
+
body = {
|
|
401
|
+
"claim_kid": record.claim_kid,
|
|
402
|
+
"topic": record.topic,
|
|
403
|
+
"oracle_outcome": record.oracle_outcome,
|
|
404
|
+
"predicted_consensus": record.predicted_consensus,
|
|
405
|
+
"rationale": record.rationale,
|
|
406
|
+
"hlc_ms": record.hlc_ms,
|
|
407
|
+
"issuer_kid": record.issuer_kid,
|
|
408
|
+
}
|
|
409
|
+
kind = "lesson"
|
|
410
|
+
elif isinstance(record, SwarmStateVector):
|
|
411
|
+
body = {
|
|
412
|
+
"topic": record.topic,
|
|
413
|
+
"vector": list(record.vector),
|
|
414
|
+
"last_hlc_ms": record.last_hlc_ms,
|
|
415
|
+
"issuer_kid": record.issuer_kid,
|
|
416
|
+
}
|
|
417
|
+
kind = "vector"
|
|
418
|
+
else:
|
|
419
|
+
raise TypeError(f"unknown swarm record: {type(record).__name__}")
|
|
420
|
+
return cbor2.dumps(
|
|
421
|
+
{_KEY_KIND: kind, _KEY_BODY: body, _KEY_VERSION: SWARM_RECORD_VERSION},
|
|
422
|
+
canonical=True,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def decode_swarm_record(
|
|
427
|
+
raw: bytes,
|
|
428
|
+
*,
|
|
429
|
+
expected_signer_kid: bytes,
|
|
430
|
+
) -> ConsensusEntry | LessonEntry | SwarmStateVector:
|
|
431
|
+
"""Decode a swarm record envelope.
|
|
432
|
+
|
|
433
|
+
The ``expected_signer_kid`` is the kid of the outer SignedStatement
|
|
434
|
+
signer; it MUST match the embedded ``issuer_kid`` in the record body
|
|
435
|
+
to prevent a kid spoofing attack where a valid signer wraps a payload
|
|
436
|
+
that falsely attributes authorship to another agent.
|
|
437
|
+
|
|
438
|
+
Raises ``ValueError`` on mismatch, oversize input, or malformed CBOR.
|
|
439
|
+
"""
|
|
440
|
+
if len(raw) > MAX_SWARM_RECORD_BYTES:
|
|
441
|
+
raise ValueError(f"swarm record too large: {len(raw)} > {MAX_SWARM_RECORD_BYTES}")
|
|
442
|
+
try:
|
|
443
|
+
env = cbor2.loads(raw)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
raise ValueError(f"swarm record decode: not CBOR: {e}") from e
|
|
446
|
+
if not isinstance(env, dict):
|
|
447
|
+
raise ValueError("swarm record decode: not a map")
|
|
448
|
+
if env.get(_KEY_VERSION) != SWARM_RECORD_VERSION:
|
|
449
|
+
raise ValueError(f"swarm record version: {env.get(_KEY_VERSION)!r}")
|
|
450
|
+
kind = env.get(_KEY_KIND)
|
|
451
|
+
body = env.get(_KEY_BODY) or {}
|
|
452
|
+
embedded_kid = bytes(body.get("issuer_kid", b""))
|
|
453
|
+
if embedded_kid != bytes(expected_signer_kid):
|
|
454
|
+
raise ValueError(
|
|
455
|
+
"issuer_kid mismatch: payload claims "
|
|
456
|
+
f"{embedded_kid[:8].hex()}, signer is "
|
|
457
|
+
f"{bytes(expected_signer_kid)[:8].hex()}"
|
|
458
|
+
)
|
|
459
|
+
if kind == "consensus":
|
|
460
|
+
return ConsensusEntry(
|
|
461
|
+
topic=str(body["topic"]),
|
|
462
|
+
posterior=float(body["posterior"]),
|
|
463
|
+
n_contributors=int(body["n_contributors"]),
|
|
464
|
+
last_hlc_ms=int(body["last_hlc_ms"]),
|
|
465
|
+
issuer_kid=embedded_kid,
|
|
466
|
+
)
|
|
467
|
+
if kind == "lesson":
|
|
468
|
+
return LessonEntry(
|
|
469
|
+
claim_kid=bytes(body["claim_kid"]),
|
|
470
|
+
topic=str(body["topic"]),
|
|
471
|
+
oracle_outcome=float(body["oracle_outcome"]),
|
|
472
|
+
predicted_consensus=float(body["predicted_consensus"]),
|
|
473
|
+
rationale=str(body.get("rationale", "")),
|
|
474
|
+
hlc_ms=int(body["hlc_ms"]),
|
|
475
|
+
issuer_kid=embedded_kid,
|
|
476
|
+
)
|
|
477
|
+
if kind == "vector":
|
|
478
|
+
return SwarmStateVector(
|
|
479
|
+
topic=str(body["topic"]),
|
|
480
|
+
vector=tuple(float(x) for x in body["vector"]),
|
|
481
|
+
last_hlc_ms=int(body["last_hlc_ms"]),
|
|
482
|
+
issuer_kid=embedded_kid,
|
|
483
|
+
)
|
|
484
|
+
raise ValueError(f"swarm record decode: unknown kind {kind!r}")
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# ---------------------------------------------------------------------------
|
|
488
|
+
# Embedding helper
|
|
489
|
+
# ---------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def compress_consensus_to_vector(
|
|
493
|
+
entries: Iterable[ConsensusEntry],
|
|
494
|
+
*,
|
|
495
|
+
dim: int = 32,
|
|
496
|
+
) -> tuple[float, ...]:
|
|
497
|
+
"""Project a set of per-topic posteriors into a fixed-dim vector.
|
|
498
|
+
|
|
499
|
+
Stable ordering by topic name; unused dimensions zero-padded.
|
|
500
|
+
Bounded at SWARM_VECTOR_MAX_DIM. Pure deterministic.
|
|
501
|
+
|
|
502
|
+
When the number of topics exceeds ``dim`` (or when ``dim`` equals
|
|
503
|
+
``SWARM_VECTOR_MAX_DIM`` and the topic count exceeds it), a
|
|
504
|
+
:class:`SwarmVectorOverflowWarning` is emitted so callers are aware
|
|
505
|
+
that extra topics are silently truncated.
|
|
506
|
+
"""
|
|
507
|
+
if dim > SWARM_VECTOR_MAX_DIM:
|
|
508
|
+
raise ValueError(f"dim {dim} > SWARM_VECTOR_MAX_DIM {SWARM_VECTOR_MAX_DIM}")
|
|
509
|
+
rows = sorted(entries, key=lambda e: e.topic)
|
|
510
|
+
n = len(rows)
|
|
511
|
+
if n > dim:
|
|
512
|
+
warnings.warn(
|
|
513
|
+
f"SwarmMemory: topic count {n} exceeds SWARM_VECTOR_MAX_DIM={SWARM_VECTOR_MAX_DIM};"
|
|
514
|
+
" extra topics truncated",
|
|
515
|
+
SwarmVectorOverflowWarning,
|
|
516
|
+
stacklevel=2,
|
|
517
|
+
)
|
|
518
|
+
out = [0.0] * dim
|
|
519
|
+
for i, e in enumerate(rows):
|
|
520
|
+
if i >= dim:
|
|
521
|
+
break
|
|
522
|
+
out[i] = float(e.posterior)
|
|
523
|
+
return tuple(out)
|