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/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)
|