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
hypermind/namespace.py ADDED
@@ -0,0 +1,515 @@
1
+ """Namespace lifecycle façade for HyperMind §24.
2
+
3
+ Implements the four wire records of the namespace lifecycle:
4
+
5
+ * ``NAMESPACE_CREATE`` — FROST-quorum-signed mint
6
+ * ``NAMESPACE_FOUNDING_ATTEST`` — public attestation of the cohort
7
+ * ``NAMESPACE_INVITE`` — single-member-signed admission grant
8
+ * ``NAMESPACE_ACCEPT`` — invitee counter-signature
9
+
10
+ This module is a thin façade over :class:`hypermind.agent._NamespaceWorld`:
11
+ it constructs the records, anchors them in the world's transparency
12
+ log, and tracks roster admission. Production deployments swap the
13
+ in-process bus + TS for libp2p + a real SCITT TS — the public verbs do
14
+ not change.
15
+
16
+ Wire-record stub: at v0.9.x boundary another agent is bumping
17
+ ``wire.py`` to v2 with the new record-type literals. We import them
18
+ defensively — if they are not yet declared we fall back to plain
19
+ strings. The merge step at the end of Phase 5 reconciles both.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import secrets
25
+ from dataclasses import dataclass, field
26
+ from typing import Any, Literal
27
+
28
+ import cbor2
29
+
30
+ from hypermind.crypto.hashing import blake3_32
31
+ from hypermind.crypto.hlc import HLC
32
+ from hypermind.crypto.namespace_root import NamespaceRoot
33
+ from hypermind.crypto.signing import Keypair
34
+ from hypermind.did_web import DidDocument, format_did
35
+ from hypermind.errors import AuthError, BlockReason, HyperMindError
36
+
37
+ # The Literal alias does not expose constants; we still need stable strings.
38
+ NAMESPACE_CREATE = "NAMESPACE_CREATE"
39
+ NAMESPACE_FOUNDING_ATTEST = "NAMESPACE_FOUNDING_ATTEST"
40
+ NAMESPACE_INVITE = "NAMESPACE_INVITE"
41
+ NAMESPACE_ACCEPT = "NAMESPACE_ACCEPT"
42
+
43
+
44
+ Visibility = Literal["public", "private", "hybrid"]
45
+ _VISIBILITY_VALUES: tuple[str, ...] = ("public", "private", "hybrid")
46
+
47
+ # Spec §24.4 default — invite validity ≤ 30 days.
48
+ INVITE_MAX_VALIDITY_MS = 30 * 24 * 3600 * 1000
49
+
50
+
51
+ def _new_namespace_id() -> bytes:
52
+ """16-byte UUIDv7-shaped namespace id (matches wire.new_namespace_id)."""
53
+ try:
54
+ from hypermind.wire import new_namespace_id
55
+
56
+ return new_namespace_id()
57
+ except ImportError: # pragma: no cover
58
+ return secrets.token_bytes(16)
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class NamespaceMember:
63
+ """Founding-cohort member record (§24.3 ``member_list[]`` entry)."""
64
+
65
+ kid: bytes
66
+ jurisdiction: str = "XX" # ISO 3166-1 alpha-2 or "XX" for undisclosed
67
+ org_did: str = ""
68
+ coi_disclosure_hash: bytes = b"\x00" * 32
69
+
70
+ def to_dict(self) -> dict[str, Any]:
71
+ return {
72
+ "kid": self.kid,
73
+ "jurisdiction": self.jurisdiction,
74
+ "org_did": self.org_did,
75
+ "coi_disclosure_hash": self.coi_disclosure_hash,
76
+ }
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class NamespaceInvite:
81
+ """A `NAMESPACE_INVITE` record (in-memory representation)."""
82
+
83
+ namespace_id: bytes
84
+ inviter_kid: bytes
85
+ invitee_kid: bytes
86
+ scope: dict[str, Any]
87
+ not_before_hlc: HLC
88
+ not_after_hlc: HLC
89
+ invite_id: bytes
90
+ signature: bytes
91
+
92
+ def canonical_payload(self) -> bytes:
93
+ return cbor2.dumps(
94
+ {
95
+ "ns_id": self.namespace_id,
96
+ "inviter_kid": self.inviter_kid,
97
+ "invitee_kid": self.invitee_kid,
98
+ "scope": self.scope,
99
+ "valid_from_hlc": self.not_before_hlc.to_cbor(),
100
+ "valid_until_hlc": self.not_after_hlc.to_cbor(),
101
+ "invite_id": self.invite_id,
102
+ },
103
+ canonical=True,
104
+ )
105
+
106
+ def hash(self) -> bytes:
107
+ return blake3_32(self.canonical_payload())
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class NamespaceAccept:
112
+ """A `NAMESPACE_ACCEPT` record (in-memory representation)."""
113
+
114
+ namespace_id: bytes
115
+ invitee_kid: bytes
116
+ invite_id: bytes
117
+ invite_hash: bytes
118
+ accepted_at_hlc: HLC
119
+ signature: bytes
120
+
121
+
122
+ @dataclass
123
+ class NamespacePolicy:
124
+ """Effective policy attached to a namespace.
125
+
126
+ v0.9.x carries only the cohort metadata + visibility. Future fields
127
+ (room admission rules, rate caps, COI thresholds) extend this dataclass
128
+ without breaking existing call sites.
129
+ """
130
+
131
+ visibility: Visibility = "private"
132
+ members: list[NamespaceMember] = field(default_factory=list)
133
+ jurisdictions: list[str] = field(default_factory=list)
134
+ coi_disclosure_hashes: list[bytes] = field(default_factory=list)
135
+
136
+
137
+ class Namespace:
138
+ """Façade over :class:`hypermind.agent._NamespaceWorld` for §24 lifecycle.
139
+
140
+ Use :meth:`create` to mint a new namespace via FROST DKG. The
141
+ resulting ``Namespace`` exposes the policy, visibility, member
142
+ roster, current epoch, and DID:web URI.
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ *,
148
+ namespace_id: bytes,
149
+ root: NamespaceRoot,
150
+ policy: NamespacePolicy,
151
+ host: str,
152
+ world: Any | None = None,
153
+ founding_keypairs: list[Keypair] | None = None,
154
+ ) -> None:
155
+ self._namespace_id = namespace_id
156
+ self._root = root
157
+ self._policy = policy
158
+ self._host = host
159
+ self._world = world # _NamespaceWorld | None
160
+ self._roster: set[bytes] = {m.kid for m in policy.members}
161
+ # Map invite_id → (NamespaceInvite, NamespaceAccept | None)
162
+ self._invites: dict[bytes, tuple[NamespaceInvite, NamespaceAccept | None]] = {}
163
+ # Founding keypairs are needed to mint NAMESPACE_INVITE records;
164
+ # we keep a (private) reference indexed by kid.
165
+ self._keypairs: dict[bytes, Keypair] = {}
166
+ for kp in founding_keypairs or []:
167
+ self._keypairs[kp.kid] = kp
168
+
169
+ # ---- properties ---------------------------------------------------
170
+
171
+ @property
172
+ def namespace_id(self) -> bytes:
173
+ return self._namespace_id
174
+
175
+ @property
176
+ def policy(self) -> NamespacePolicy:
177
+ return self._policy
178
+
179
+ @property
180
+ def visibility(self) -> Visibility:
181
+ return self._policy.visibility
182
+
183
+ @property
184
+ def members(self) -> list[bytes]:
185
+ """Current roster — founding members plus admitted invitees."""
186
+ return list(self._roster)
187
+
188
+ @property
189
+ def epoch(self):
190
+ return self._root.epoch
191
+
192
+ @property
193
+ def root(self) -> NamespaceRoot:
194
+ return self._root
195
+
196
+ @property
197
+ def did(self) -> str:
198
+ return format_did(self._host, self._namespace_id)
199
+
200
+ # ---- factory ------------------------------------------------------
201
+
202
+ @classmethod
203
+ def create(
204
+ cls,
205
+ founding_keypairs: list[Keypair],
206
+ k: int = 5,
207
+ n: int = 7,
208
+ visibility: Visibility = "private",
209
+ *,
210
+ host: str = "localhost",
211
+ jurisdictions: list[str] | None = None,
212
+ coi_disclosure_hashes: list[bytes] | None = None,
213
+ org_dids: list[str] | None = None,
214
+ world: Any | None = None,
215
+ ) -> Namespace:
216
+ """Mint a new namespace via FROST DKG (spec §24.2 / §24.3).
217
+
218
+ Steps:
219
+ 1. Run :meth:`NamespaceRoot.from_dkg` over the founders.
220
+ 2. Build the ``NAMESPACE_FOUNDING_ATTEST`` payload.
221
+ 3. Build the ``NAMESPACE_CREATE`` payload (FROST-signed).
222
+ 4. Anchor both in the world's transparency log if a world
223
+ is supplied.
224
+ 5. Register in the global namespace registry.
225
+ """
226
+ if visibility not in _VISIBILITY_VALUES:
227
+ raise ValueError(f"visibility must be one of {_VISIBILITY_VALUES}, got {visibility!r}")
228
+ if len(founding_keypairs) != n:
229
+ raise ValueError(f"founding_keypairs count {len(founding_keypairs)} != n={n}")
230
+ # Spec §24.2: k >= ceil(2n/3), n >= 3 — delegated to NamespaceRoot.from_dkg.
231
+ root = NamespaceRoot.from_dkg(founding_keypairs, k=k, n=n)
232
+
233
+ jurisdictions = jurisdictions or ["XX"] * n
234
+ coi_disclosure_hashes = coi_disclosure_hashes or [b"\x00" * 32] * n
235
+ org_dids = org_dids or [""] * n
236
+ if not (len(jurisdictions) == len(coi_disclosure_hashes) == len(org_dids) == n):
237
+ raise ValueError(
238
+ "jurisdictions / coi_disclosure_hashes / org_dids must each have length n"
239
+ )
240
+
241
+ members = [
242
+ NamespaceMember(
243
+ kid=kp.kid,
244
+ jurisdiction=jurisdictions[i],
245
+ org_did=org_dids[i],
246
+ coi_disclosure_hash=coi_disclosure_hashes[i],
247
+ )
248
+ for i, kp in enumerate(founding_keypairs)
249
+ ]
250
+ policy = NamespacePolicy(
251
+ visibility=visibility,
252
+ members=members,
253
+ jurisdictions=list(jurisdictions),
254
+ coi_disclosure_hashes=list(coi_disclosure_hashes),
255
+ )
256
+
257
+ ns_id = _new_namespace_id()
258
+ created_hlc = HLC.now(node_id=founding_keypairs[0].kid[:8])
259
+
260
+ # ---- NAMESPACE_FOUNDING_ATTEST payload ------------------------
261
+ attest_body = {
262
+ "ns_id": ns_id,
263
+ "members": [m.to_dict() for m in members],
264
+ "dkg_transcript_hash": root.dkg_transcript_hash,
265
+ "root_pubkey": root.group_pubkey,
266
+ "visibility": visibility,
267
+ "created_at_hlc": created_hlc.to_cbor(),
268
+ # Reserved (spec §24.3): MUST be present as null in v0.9.x.
269
+ "epoch_rotation_proof": None,
270
+ "mls_group_id": None,
271
+ }
272
+ attest_preimage = cbor2.dumps(attest_body, canonical=True)
273
+ attest_sig = root.sign(attest_preimage, signer_subset=list(range(1, k + 1)))
274
+ attest_record = {**attest_body, "frost_signature": attest_sig}
275
+
276
+ # ---- NAMESPACE_CREATE payload --------------------------------
277
+ create_body = {
278
+ "ns_id": ns_id,
279
+ "founding_cohort": [kp.kid for kp in founding_keypairs],
280
+ "threshold": {"k": k, "n": n},
281
+ "dkg_transcript_hash": root.dkg_transcript_hash,
282
+ "root_pubkey": root.group_pubkey,
283
+ "visibility": visibility,
284
+ "created_at_hlc": created_hlc.to_cbor(),
285
+ }
286
+ create_preimage = cbor2.dumps(create_body, canonical=True)
287
+ create_sig = root.sign(create_preimage, signer_subset=list(range(1, k + 1)))
288
+ create_record = {**create_body, "frost_signature": create_sig}
289
+
290
+ # ---- anchor in transparency log if world available -----------
291
+ if world is not None:
292
+ ts = getattr(world, "ts", None)
293
+ if ts is not None:
294
+ # Anchor the BLAKE3 hash of each canonical record as a leaf
295
+ # kid (§04 transparency uses 32-byte leaf identifiers).
296
+ attest_kid = blake3_32(cbor2.dumps(attest_record, canonical=True))
297
+ create_kid = blake3_32(cbor2.dumps(create_record, canonical=True))
298
+ try:
299
+ ts.append_and_cosign(attest_kid)
300
+ ts.append_and_cosign(create_kid)
301
+ except Exception:
302
+ pass
303
+
304
+ ns = cls(
305
+ namespace_id=ns_id,
306
+ root=root,
307
+ policy=policy,
308
+ host=host,
309
+ world=world,
310
+ founding_keypairs=founding_keypairs,
311
+ )
312
+ # Stash the raw records for tests / audit (private attributes).
313
+ ns._founding_attest = attest_record
314
+ ns._namespace_create = create_record
315
+ return ns
316
+
317
+ # ---- invite / accept ---------------------------------------------
318
+
319
+ def invite(
320
+ self,
321
+ invitee_kid: bytes,
322
+ scope: dict[str, Any],
323
+ valid_window_hlc: tuple[HLC, HLC],
324
+ *,
325
+ inviter_kid: bytes | None = None,
326
+ ) -> NamespaceInvite:
327
+ """Issue a `NAMESPACE_INVITE` (§24.4).
328
+
329
+ Signed by an existing authorised member. ``inviter_kid``
330
+ defaults to the first founding member; in production an L5
331
+ admit-authority CapToken would gate this.
332
+ """
333
+ not_before, not_after = valid_window_hlc
334
+ if not_after.logical_ms < not_before.logical_ms:
335
+ raise ValueError("valid_window_hlc: not_after_hlc precedes not_before_hlc")
336
+ delta_ms = not_after.logical_ms - not_before.logical_ms
337
+ if delta_ms > INVITE_MAX_VALIDITY_MS:
338
+ raise ValueError(
339
+ f"invite validity window {delta_ms}ms exceeds spec max "
340
+ f"{INVITE_MAX_VALIDITY_MS}ms (§24.4)"
341
+ )
342
+ if inviter_kid is None:
343
+ inviter_kid = self._policy.members[0].kid
344
+ if inviter_kid not in self._roster:
345
+ raise AuthError(
346
+ "inviter is not a member of the namespace",
347
+ reason=BlockReason(
348
+ rule_id="NS-INVITE-AUTH",
349
+ spec_ref="§24.4",
350
+ hint="Inviter kid must be in the current roster.",
351
+ ),
352
+ )
353
+ kp = self._keypairs.get(inviter_kid)
354
+ if kp is None:
355
+ raise AuthError(
356
+ "no keypair available to sign invite",
357
+ reason=BlockReason(
358
+ rule_id="NS-INVITE-NO-KEY",
359
+ spec_ref="§24.4",
360
+ hint="Founding keypair was not registered with this Namespace facade.",
361
+ ),
362
+ )
363
+
364
+ invite_id = secrets.token_bytes(16)
365
+ unsigned = {
366
+ "ns_id": self._namespace_id,
367
+ "inviter_kid": inviter_kid,
368
+ "invitee_kid": invitee_kid,
369
+ "scope": scope,
370
+ "valid_from_hlc": not_before.to_cbor(),
371
+ "valid_until_hlc": not_after.to_cbor(),
372
+ "invite_id": invite_id,
373
+ }
374
+ signature = kp.sign(cbor2.dumps(unsigned, canonical=True))
375
+ invite = NamespaceInvite(
376
+ namespace_id=self._namespace_id,
377
+ inviter_kid=inviter_kid,
378
+ invitee_kid=invitee_kid,
379
+ scope=scope,
380
+ not_before_hlc=not_before,
381
+ not_after_hlc=not_after,
382
+ invite_id=invite_id,
383
+ signature=signature,
384
+ )
385
+ self._invites[invite_id] = (invite, None)
386
+ # Anchor the invite hash in TS (best-effort).
387
+ if self._world is not None:
388
+ ts = getattr(self._world, "ts", None)
389
+ if ts is not None:
390
+ try:
391
+ ts.append_and_cosign(invite.hash())
392
+ except Exception:
393
+ pass
394
+ return invite
395
+
396
+ def accept(self, invite: NamespaceInvite, invitee_kp: Keypair) -> NamespaceAccept:
397
+ """Counter-sign an invite (§24.5) and complete admission.
398
+
399
+ Spec §24.5: admission completes only when both records appear in
400
+ the transparency log. This implementation mirrors that — the
401
+ invitee is added to the roster only if the original invite is
402
+ registered with this Namespace and is within its validity window.
403
+ """
404
+ if invitee_kp.kid != invite.invitee_kid:
405
+ raise AuthError(
406
+ "accept signed by wrong kid",
407
+ reason=BlockReason(
408
+ rule_id="NS-ACCEPT-WRONG-KID",
409
+ spec_ref="§24.5",
410
+ hint="The signing kid MUST equal NAMESPACE_INVITE.invitee_kid.",
411
+ ),
412
+ )
413
+ registered = self._invites.get(invite.invite_id)
414
+ if registered is None:
415
+ raise HyperMindError(
416
+ BlockReason(
417
+ rule_id="NS-ACCEPT-NO-INVITE",
418
+ spec_ref="§24.5",
419
+ hint="No matching NAMESPACE_INVITE on file for this invite_id.",
420
+ )
421
+ )
422
+ # Check validity window against current HLC.
423
+ now = HLC.now(node_id=invitee_kp.kid[:8])
424
+ if now.logical_ms < invite.not_before_hlc.logical_ms:
425
+ raise HyperMindError(
426
+ BlockReason(
427
+ rule_id="NS-ACCEPT-NOT-YET-VALID",
428
+ spec_ref="§24.4",
429
+ hint="Invite is not yet within its validity window.",
430
+ )
431
+ )
432
+ if now.logical_ms > invite.not_after_hlc.logical_ms:
433
+ raise HyperMindError(
434
+ BlockReason(
435
+ rule_id="NS-ACCEPT-EXPIRED",
436
+ spec_ref="§24.4",
437
+ hint="Invite has expired (not_after_hlc passed).",
438
+ )
439
+ )
440
+
441
+ invite_hash = invite.hash()
442
+ unsigned = {
443
+ "ns_id": self._namespace_id,
444
+ "invitee_kid": invitee_kp.kid,
445
+ "invite_id": invite.invite_id,
446
+ "invite_hash": invite_hash,
447
+ "accepted_at_hlc": now.to_cbor(),
448
+ }
449
+ signature = invitee_kp.sign(cbor2.dumps(unsigned, canonical=True))
450
+ accept = NamespaceAccept(
451
+ namespace_id=self._namespace_id,
452
+ invitee_kid=invitee_kp.kid,
453
+ invite_id=invite.invite_id,
454
+ invite_hash=invite_hash,
455
+ accepted_at_hlc=now,
456
+ signature=signature,
457
+ )
458
+ # Admission completes — both records present.
459
+ self._invites[invite.invite_id] = (invite, accept)
460
+ self._roster.add(invitee_kp.kid)
461
+
462
+ if self._world is not None:
463
+ ts = getattr(self._world, "ts", None)
464
+ if ts is not None:
465
+ try:
466
+ ts.append_and_cosign(blake3_32(cbor2.dumps(unsigned, canonical=True)))
467
+ except Exception:
468
+ pass
469
+ return accept
470
+
471
+ # ---- visibility / payload gating ---------------------------------
472
+
473
+ def require_l3_envelope(self) -> bool:
474
+ """True iff payload records MUST be L3-encrypted (§24.7)."""
475
+ return self._policy.visibility in ("private", "hybrid")
476
+
477
+ def gate_plaintext_payload(self, statement: Any) -> None:
478
+ """Raise if plaintext payload arrives in a non-public namespace.
479
+
480
+ Future work: when L5 separability lands, this gate should run on
481
+ the SignedStatement ingest path so receivers automatically emit
482
+ ``BlockReason("CASCADE-L3-PLAINTEXT-IN-PRIVATE-NS")`` (spec §24.7).
483
+ """
484
+ if self.require_l3_envelope():
485
+ raise HyperMindError(
486
+ BlockReason(
487
+ rule_id="CASCADE-L3-PLAINTEXT-IN-PRIVATE-NS",
488
+ spec_ref="§24.7",
489
+ hint=(
490
+ f"namespace visibility={self._policy.visibility} requires "
491
+ "L3-encrypted payloads; received plaintext SignedStatement."
492
+ ),
493
+ )
494
+ )
495
+
496
+ def build_did_document(self, gossip_endpoints: list[str]) -> DidDocument:
497
+ """Build the DID:web rendezvous document (§24.6)."""
498
+ return DidDocument.build(
499
+ ns=self._root,
500
+ host=self._host,
501
+ gossip_endpoints=gossip_endpoints,
502
+ ns_id=self._namespace_id,
503
+ )
504
+
505
+ # ---- introspection -----------------------------------------------
506
+
507
+ @property
508
+ def founding_attest(self) -> dict[str, Any]:
509
+ """The signed `NAMESPACE_FOUNDING_ATTEST` record (read-only view)."""
510
+ return dict(self._founding_attest)
511
+
512
+ @property
513
+ def namespace_create(self) -> dict[str, Any]:
514
+ """The signed `NAMESPACE_CREATE` record (read-only view)."""
515
+ return dict(self._namespace_create)