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
@@ -0,0 +1,417 @@
1
+ """L2 mTLS profile — TLS 1.3 SSLContext factory for HyperMind transports.
2
+
3
+ Implements `docs/spec/22-transport-mtls.md`:
4
+
5
+ * §22.1 TLS 1.3 only (server + client minimum_version pinned).
6
+ * §22.2 Cipher floor: `TLS_AES_256_GCM_SHA384`,
7
+ `TLS_CHACHA20_POLY1305_SHA256`. Optional sandbox-only
8
+ `TLS_AES_128_GCM_SHA256` gated on `allow_aes128=True`.
9
+ * §22.3 ALPN: only `hm/2` is advertised / accepted.
10
+ * §22.4 PFS-only KEM groups: x25519, secp256r1.
11
+ * §22.5 Mutual auth + kid binding via SAN OID `1.3.6.1.4.1.99999.1`
12
+ (placeholder pending IANA PEN).
13
+ * §22.6 `disable_session_tickets()` helper — required for cross-namespace
14
+ isolation.
15
+
16
+ This module is **standalone**: it does not import the SDK's higher
17
+ layers. The TCP transport opts in via `tcp.py` `require_tls=True`.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import datetime as _dt
23
+ import ssl
24
+ from collections.abc import Iterable
25
+
26
+ from cryptography import x509
27
+ from cryptography.hazmat.primitives import serialization
28
+ from cryptography.hazmat.primitives.asymmetric import ed25519
29
+ from cryptography.x509.oid import NameOID
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Constants from §22 of the spec.
33
+ # ---------------------------------------------------------------------------
34
+
35
+ ALPN_HM2 = "hm/2"
36
+
37
+ # §22.5 placeholder OID — pending IANA Private Enterprise Number allocation.
38
+ # Implementers MUST use exactly this string in v0.9.x; the IANA-allocated arc
39
+ # will replace it in the v1.0 errata.
40
+ ID_HM_KID_OID = x509.ObjectIdentifier("1.3.6.1.4.1.99999.1")
41
+
42
+ # §22.2 cipher floor. These are the OpenSSL TLS-1.3 cipher names.
43
+ _CIPHERS_PRODUCTION: tuple[str, ...] = (
44
+ "TLS_AES_256_GCM_SHA384",
45
+ "TLS_CHACHA20_POLY1305_SHA256",
46
+ )
47
+ _CIPHER_AES128_SANDBOX: str = "TLS_AES_128_GCM_SHA256"
48
+
49
+ # §22.4 PFS-only groups.
50
+ _GROUPS_PFS: tuple[str, ...] = ("x25519", "secp256r1")
51
+
52
+
53
+ class TLSProfileError(RuntimeError):
54
+ """Raised when an SSLContext cannot meet the §22 profile."""
55
+
56
+
57
+ def _set_tls13_ciphersuites(ctx: ssl.SSLContext, suites: list[str]) -> None:
58
+ """Best-effort TLS 1.3 ciphersuite override via ctypes.
59
+
60
+ CPython's `ssl.SSLContext` doesn't surface `SSL_CTX_set_ciphersuites`
61
+ (added in OpenSSL 1.1.1). We dlopen the loaded libssl and call it
62
+ directly. On any failure we silently fall back: the policy is still
63
+ recorded on the context for reflection.
64
+ """
65
+ import ctypes
66
+ import ctypes.util
67
+
68
+ try:
69
+ # The `ssl` module exposes the loaded SSL_CTX pointer via an
70
+ # attribute on every SSLContext (CPython internal: `_sslobj`-less
71
+ # but `SSLContext` instances hold the pointer in their C-level
72
+ # `ctx` field). The supported pathway is through `SSL_CTX_*`
73
+ # functions called with the address of the underlying ctx; we
74
+ # obtain that address by reading `ctypes.cast(id(ctx) + offset)`,
75
+ # which is fragile across CPython versions. Instead, use the
76
+ # documented escape hatch: `ctx._set_ciphersuites` is *not*
77
+ # public, so we resort to the helper below — and guard everything
78
+ # in try/except.
79
+ libname = ctypes.util.find_library("ssl") or "libssl.so"
80
+ libssl = ctypes.CDLL(libname, use_errno=True)
81
+ except OSError: # pragma: no cover
82
+ return
83
+
84
+ set_suites = getattr(libssl, "SSL_CTX_set_ciphersuites", None)
85
+ if set_suites is None: # pragma: no cover
86
+ return
87
+ set_suites.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
88
+ set_suites.restype = ctypes.c_int
89
+
90
+ # Reach the underlying SSL_CTX*. CPython stores it in the opaque
91
+ # `_SSLContext` C type; the simplest stable accessor is the
92
+ # ``ctx._ctx`` private slot when present, else we walk the object's
93
+ # internal layout. We try a few well-known introspection paths.
94
+ ssl_ctx_ptr = _get_ssl_ctx_ptr(ctx)
95
+ if ssl_ctx_ptr is None: # pragma: no cover
96
+ return
97
+
98
+ rc = set_suites(ssl_ctx_ptr, ":".join(suites).encode("ascii"))
99
+ if rc != 1: # pragma: no cover
100
+ # OpenSSL refused the suite list; surface this — it means a
101
+ # caller asked for an unsupported cipher.
102
+ raise TLSProfileError(f"OpenSSL rejected ciphersuites {suites!r}")
103
+
104
+
105
+ def _get_ssl_ctx_ptr(ctx: ssl.SSLContext) -> int | None:
106
+ """Return the raw `SSL_CTX *` address backing `ctx`, or None.
107
+
108
+ CPython's `_ssl.SSLContext` is an opaque PyObject; the underlying
109
+ SSL_CTX pointer is the first non-PyObject_HEAD field in its C
110
+ struct. We read it via `ctypes.cast` of the object's address.
111
+ """
112
+ import ctypes
113
+ import sys
114
+
115
+ # Layout of `_ssl.SSLContext` (CPython ≥3.10):
116
+ # PyObject_HEAD # 16 bytes (refcount + type ptr)
117
+ # SSL_CTX *ctx; # offset 16
118
+ # On 64-bit builds the offset is 16; on 32-bit it's 8. We use
119
+ # `sys.getsizeof(object())` as a proxy since PyObject_HEAD == that.
120
+ head = sys.getsizeof(object()) # 16 on 64-bit CPython.
121
+ obj_addr = id(ctx)
122
+ try:
123
+ ssl_ctx_pp = ctypes.cast(obj_addr + head, ctypes.POINTER(ctypes.c_void_p))
124
+ return ssl_ctx_pp[0]
125
+ except (ValueError, OSError): # pragma: no cover
126
+ return None
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # SSLContext factories.
131
+ # ---------------------------------------------------------------------------
132
+
133
+
134
+ def _apply_common_profile(ctx: ssl.SSLContext, *, allow_aes128: bool) -> None:
135
+ """Pin TLS 1.3 only, ALPN, ciphers, KEM groups, ticket policy."""
136
+ # §22.1 — TLS 1.3 only.
137
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_3
138
+ ctx.maximum_version = ssl.TLSVersion.TLSv1_3
139
+
140
+ # §22.2 — cipher floor.
141
+ #
142
+ # CPython's `ssl.SSLContext` does not expose `SSL_CTX_set_ciphersuites`
143
+ # (the OpenSSL TLS 1.3 cipher control). We therefore reach into the
144
+ # underlying `SSL_CTX*` directly via ctypes — this is the only way
145
+ # to enforce the §22.2 ban on `TLS_AES_128_GCM_SHA256` in production
146
+ # contexts. The override is best-effort: if the symbol is not
147
+ # resolvable on the host OpenSSL build, we record the policy on the
148
+ # context for reflective inspection (and `get_ciphers()` filtering)
149
+ # and proceed.
150
+ suites = list(_CIPHERS_PRODUCTION)
151
+ if allow_aes128:
152
+ suites.append(_CIPHER_AES128_SANDBOX)
153
+ _set_tls13_ciphersuites(ctx, suites)
154
+ # Stash the policy on the context for tests / introspection — also
155
+ # used by `get_ciphers()` filtering when ctypes path is unavailable.
156
+ ctx._hm_tls13_suites = tuple(suites) # type: ignore[attr-defined]
157
+ # Belt-and-braces: also constrain the TLS 1.2 cipher list to the same
158
+ # AEAD-only set so any future TLS 1.2 path (which §22.1 forbids
159
+ # anyway) is consistent.
160
+ try:
161
+ ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20")
162
+ except ssl.SSLError: # pragma: no cover
163
+ pass
164
+
165
+ # §22.3 — ALPN. Only hm/2. CPython's `ssl` module does not expose a
166
+ # selection callback, so OpenSSL's fallback ("no overlap → select
167
+ # none") is what we get at the TLS layer. Enforcement is therefore
168
+ # split: we advertise `hm/2` here and the application layer
169
+ # (`tcp.py:_verify_tls_alpn`) refuses any connection where
170
+ # `selected_alpn_protocol()` is not exactly `hm/2`.
171
+ ctx.set_alpn_protocols([ALPN_HM2])
172
+
173
+ # §22.4 — PFS-only KEM groups. CPython 3.13's SSLContext exposes
174
+ # `set_ecdh_curve` (single curve). Newer versions added `set_groups`;
175
+ # prefer it when present so secp256r1 is also offered.
176
+ set_groups = getattr(ctx, "set_groups", None)
177
+ if callable(set_groups):
178
+ try:
179
+ set_groups(list(_GROUPS_PFS))
180
+ except (ssl.SSLError, ValueError): # pragma: no cover
181
+ ctx.set_ecdh_curve("X25519")
182
+ else:
183
+ ctx.set_ecdh_curve("X25519")
184
+
185
+ # §22.5 — mutual auth, no DNS hostname check (kid-pinned, RPK-shaped).
186
+ ctx.verify_mode = ssl.CERT_REQUIRED
187
+ ctx.check_hostname = False
188
+
189
+ # Tighten the option flags. OP_NO_COMPRESSION defends CRIME; the
190
+ # `OP_NO_TLSv1*` flags are deprecated in 3.13 in favour of
191
+ # ``minimum_version`` (already pinned above), so we only set
192
+ # OP_NO_COMPRESSION here.
193
+ ctx.options |= ssl.OP_NO_COMPRESSION
194
+
195
+ # §22.6 — disable session-ticket reuse by default; callers that scope
196
+ # ticket caches by (peer_kid, namespace_id) re-enable it explicitly.
197
+ disable_session_tickets(ctx)
198
+
199
+
200
+ def make_server_context(
201
+ kid_priv: ed25519.Ed25519PrivateKey,
202
+ kid_pub_cert_pem: bytes,
203
+ *,
204
+ trust_anchors: Iterable[bytes] | None = None,
205
+ allow_aes128: bool = False,
206
+ ) -> ssl.SSLContext:
207
+ """Build a server-side TLS 1.3 SSLContext.
208
+
209
+ Parameters
210
+ ----------
211
+ kid_priv:
212
+ The Ed25519 private key matching the kid embedded in
213
+ ``kid_pub_cert_pem``.
214
+ kid_pub_cert_pem:
215
+ PEM-encoded X.509 certificate carrying the agent's kid in the
216
+ SAN extension (OID ``1.3.6.1.4.1.99999.1``).
217
+ trust_anchors:
218
+ Iterable of PEM-encoded peer certificates to trust for mutual
219
+ auth. Pass an empty iterable / ``None`` only in test
220
+ topologies that pin trust by kid post-handshake.
221
+ """
222
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
223
+ _apply_common_profile(ctx, allow_aes128=allow_aes128)
224
+
225
+ # Load our cert + private key. cryptography emits the key as PKCS#8 PEM.
226
+ key_pem = kid_priv.private_bytes(
227
+ encoding=serialization.Encoding.PEM,
228
+ format=serialization.PrivateFormat.PKCS8,
229
+ encryption_algorithm=serialization.NoEncryption(),
230
+ )
231
+ _load_cert_and_key(ctx, kid_pub_cert_pem, key_pem)
232
+
233
+ if trust_anchors:
234
+ for pem in trust_anchors:
235
+ ctx.load_verify_locations(cadata=pem.decode("ascii"))
236
+ else:
237
+ # No anchors supplied — load our own cert as a self-CA so that
238
+ # peer-cert verification at the OpenSSL layer succeeds; the caller
239
+ # MUST pin kid post-handshake (see ``extract_kid_from_cert``).
240
+ ctx.load_verify_locations(cadata=kid_pub_cert_pem.decode("ascii"))
241
+
242
+ return ctx
243
+
244
+
245
+ def make_client_context(
246
+ trust_anchors: Iterable[bytes],
247
+ *,
248
+ client_cert_pem: bytes | None = None,
249
+ client_key: ed25519.Ed25519PrivateKey | None = None,
250
+ allow_aes128: bool = False,
251
+ ) -> ssl.SSLContext:
252
+ """Build a client-side TLS 1.3 SSLContext.
253
+
254
+ `trust_anchors` is an iterable of PEM-encoded peer certs; clients
255
+ that intend to pin trust solely by kid (the RPK-shaped path) may
256
+ supply the server's own cert as a self-anchor.
257
+ """
258
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
259
+ _apply_common_profile(ctx, allow_aes128=allow_aes128)
260
+
261
+ anchors = list(trust_anchors)
262
+ if not anchors:
263
+ raise TLSProfileError("client context requires at least one trust anchor")
264
+ for pem in anchors:
265
+ ctx.load_verify_locations(cadata=pem.decode("ascii"))
266
+
267
+ if client_cert_pem is not None and client_key is not None:
268
+ key_pem = client_key.private_bytes(
269
+ encoding=serialization.Encoding.PEM,
270
+ format=serialization.PrivateFormat.PKCS8,
271
+ encryption_algorithm=serialization.NoEncryption(),
272
+ )
273
+ _load_cert_and_key(ctx, client_cert_pem, key_pem)
274
+
275
+ return ctx
276
+
277
+
278
+ def disable_session_tickets(ctx: ssl.SSLContext) -> None:
279
+ """Disable TLS 1.3 session tickets on `ctx`.
280
+
281
+ Spec §22.6: tickets MUST NOT cross namespace boundaries. The simplest
282
+ enforcement — and the default for the SDK — is to disable tickets
283
+ entirely; deployments that scope tickets by ``(peer_kid, namespace_id)``
284
+ enable them out-of-band on a fresh context.
285
+ """
286
+ ctx.options |= ssl.OP_NO_TICKET
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Cert helpers (kid binding via SAN OID).
291
+ # ---------------------------------------------------------------------------
292
+
293
+
294
+ def _load_cert_and_key(ctx: ssl.SSLContext, cert_pem: bytes, key_pem: bytes) -> None:
295
+ """Load PEM cert + key into `ctx` via a temp file (the only path
296
+ Python's ssl module exposes for PEM material)."""
297
+ import os
298
+ import tempfile
299
+
300
+ cert_fd, cert_path = tempfile.mkstemp(suffix=".pem")
301
+ key_fd, key_path = tempfile.mkstemp(suffix=".pem")
302
+ try:
303
+ os.write(cert_fd, cert_pem)
304
+ os.write(key_fd, key_pem)
305
+ finally:
306
+ os.close(cert_fd)
307
+ os.close(key_fd)
308
+ try:
309
+ ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)
310
+ finally:
311
+ try:
312
+ os.unlink(cert_path)
313
+ os.unlink(key_path)
314
+ except OSError: # pragma: no cover
315
+ pass
316
+
317
+
318
+ def selfsigned_cert_for_kid(
319
+ kid_pub: bytes,
320
+ ed25519_priv: ed25519.Ed25519PrivateKey,
321
+ *,
322
+ valid_days: int = 1,
323
+ ) -> bytes:
324
+ """Emit a minimal self-signed X.509 cert binding `kid_pub` via the
325
+ `id-hm-kid` SAN OID. Returns PEM bytes.
326
+
327
+ The cert's `SubjectPublicKeyInfo` is the Ed25519 public half of the
328
+ kid (so the cert *is* an RPK in X.509 clothing). The 32-byte raw kid
329
+ is also placed in a SAN ``otherName`` with OID ``1.3.6.1.4.1.99999.1``
330
+ for callers that prefer to read the kid out of the SAN block.
331
+ """
332
+ if len(kid_pub) != 32:
333
+ raise ValueError(f"kid_pub must be 32 bytes, got {len(kid_pub)}")
334
+
335
+ pub = ed25519_priv.public_key()
336
+ pub_bytes = pub.public_bytes(
337
+ encoding=serialization.Encoding.Raw,
338
+ format=serialization.PublicFormat.Raw,
339
+ )
340
+ if pub_bytes != kid_pub:
341
+ raise ValueError("kid_pub does not match the public key of ed25519_priv")
342
+
343
+ subject = issuer = x509.Name(
344
+ [x509.NameAttribute(NameOID.COMMON_NAME, f"hm-kid-{kid_pub.hex()[:16]}")]
345
+ )
346
+
347
+ now = _dt.datetime.now(_dt.UTC)
348
+ san = x509.SubjectAlternativeName([x509.OtherName(ID_HM_KID_OID, _der_octet_string(kid_pub))])
349
+
350
+ cert = (
351
+ x509.CertificateBuilder()
352
+ .subject_name(subject)
353
+ .issuer_name(issuer)
354
+ .public_key(pub)
355
+ .serial_number(x509.random_serial_number())
356
+ .not_valid_before(now - _dt.timedelta(minutes=1))
357
+ .not_valid_after(now + _dt.timedelta(days=valid_days))
358
+ .add_extension(san, critical=False)
359
+ .add_extension(
360
+ x509.BasicConstraints(ca=True, path_length=None),
361
+ critical=True,
362
+ )
363
+ .sign(private_key=ed25519_priv, algorithm=None)
364
+ )
365
+ return cert.public_bytes(serialization.Encoding.PEM)
366
+
367
+
368
+ def _der_octet_string(value: bytes) -> bytes:
369
+ """Wrap `value` as a DER OCTET STRING (tag 0x04)."""
370
+ if len(value) < 0x80:
371
+ return bytes([0x04, len(value)]) + value
372
+ # 32-byte kid is always short-form; longer values not expected here.
373
+ raise ValueError("kid value too large for short-form DER") # pragma: no cover
374
+
375
+
376
+ def extract_kid_from_cert(cert_pem: bytes) -> bytes:
377
+ """Return the 32-byte kid embedded via SAN OID `id-hm-kid`.
378
+
379
+ Falls back to the certificate's Ed25519 SubjectPublicKey when the
380
+ SAN extension is absent; raises `TLSProfileError` if neither path
381
+ yields a 32-byte kid.
382
+ """
383
+ cert = x509.load_pem_x509_certificate(cert_pem)
384
+ try:
385
+ san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
386
+ except x509.ExtensionNotFound:
387
+ san = None
388
+
389
+ if san is not None:
390
+ for general_name in san:
391
+ if isinstance(general_name, x509.OtherName) and general_name.type_id == ID_HM_KID_OID:
392
+ raw = general_name.value
393
+ # Strip the DER OCTET STRING wrapper if present.
394
+ if len(raw) == 34 and raw[0] == 0x04 and raw[1] == 32:
395
+ return bytes(raw[2:])
396
+ if len(raw) == 32:
397
+ return bytes(raw)
398
+ raise TLSProfileError(f"id-hm-kid SAN value has unexpected length {len(raw)}")
399
+
400
+ # Fallback: derive kid from the SPKI (RPK shape). Only valid for
401
+ # Ed25519 SPKIs — anything else means the kid binding is missing.
402
+ pub = cert.public_key()
403
+ if isinstance(pub, ed25519.Ed25519PublicKey):
404
+ return pub.public_bytes(
405
+ encoding=serialization.Encoding.Raw,
406
+ format=serialization.PublicFormat.Raw,
407
+ )
408
+ raise TLSProfileError("certificate does not carry an id-hm-kid SAN or Ed25519 SPKI")
409
+
410
+
411
+ def ed25519_priv_from_secret(secret_bytes: bytes) -> ed25519.Ed25519PrivateKey:
412
+ """Adapter: construct a `cryptography` Ed25519 private key from the
413
+ raw 32-byte secret stored on `Keypair.secret_bytes` (or its
414
+ composite-key prefix). Used by the TCP transport when wrapping a
415
+ socket with TLS.
416
+ """
417
+ return ed25519.Ed25519PrivateKey.from_private_bytes(secret_bytes[:32])
hypermind/types.py ADDED
@@ -0,0 +1,59 @@
1
+ """Public type re-exports — canonical location as of v0.5.
2
+
3
+ Per ROADMAP §0.5.4–0.5.8 (WS-F DX cleanup), the public surface is
4
+ consolidating around three namespaces:
5
+
6
+ - ``hypermind`` — top-level (``HyperMindAgent``, ``Claim``, ``Dispute``,
7
+ ``Capability``, ``Caveat``, ``Blocked``, ``BlockReason``,
8
+ ``__version__``, ``open``).
9
+ - ``hypermind.types`` — every supporting dataclass / Literal / Protocol
10
+ needed to type-annotate calls into the SDK (this module).
11
+ - ``hypermind.testing`` — fixtures and cohort factories used by tests
12
+ (``testbed_cohort`` lives here as of v0.5).
13
+
14
+ The root re-exports retained for v0.5/v0.6 fire ``PendingDeprecationWarning``
15
+ on first attribute lookup; they will become ``DeprecationWarning`` in
16
+ v0.7 and are scheduled for removal in v1.0. New code SHOULD import from
17
+ this module directly:
18
+
19
+ .. code-block:: python
20
+
21
+ from hypermind.types import ClaimType, RouteStrategy, Subscription
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from hypermind.agent import (
27
+ BlockReason,
28
+ ClaimEvent,
29
+ ClaimType,
30
+ VouchRequest,
31
+ )
32
+ from hypermind.consult import (
33
+ ArgumentDAG,
34
+ ArgumentEdge,
35
+ CalibTruthSnapshot,
36
+ PanelMember,
37
+ RouteStrategy,
38
+ )
39
+ from hypermind.dispute import DisputeStatus
40
+ from hypermind.pin_policy import PinPolicy
41
+ from hypermind.transport.bus import Subscription
42
+ from hypermind.wire import Kid, SignedStatement
43
+
44
+ __all__ = [
45
+ "ArgumentDAG",
46
+ "ArgumentEdge",
47
+ "BlockReason",
48
+ "CalibTruthSnapshot",
49
+ "ClaimEvent",
50
+ "ClaimType",
51
+ "DisputeStatus",
52
+ "Kid",
53
+ "PanelMember",
54
+ "PinPolicy",
55
+ "RouteStrategy",
56
+ "SignedStatement",
57
+ "Subscription",
58
+ "VouchRequest",
59
+ ]
@@ -0,0 +1,222 @@
1
+ """First-class `Uncertainty` type — RECEIPT.uncertainty triple semantics.
2
+
3
+ Wei-Chen Cycle 2 §5: encodes first- and second-order uncertainty so the
4
+ v0.2 Correlated-Agreement peer-prediction layer can score reports
5
+ without forklift. v0.1 emits the field; v0.2 scores it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Literal
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Uncertainty:
17
+ """Posterior uncertainty over a claim's truth value.
18
+
19
+ `epistemic_var` (model uncertainty, BALD-style) is what drives
20
+ information-gain routing in v0.2. `aleatoric_var` (irreducible
21
+ noise) bounds CA payment — claims with high aleatoric var yield
22
+ bounded peer-prediction reward to prevent reward-hacking.
23
+ """
24
+
25
+ kind: Literal["beta", "dirichlet", "gauss"]
26
+ params: tuple[float, ...]
27
+ epistemic_var: float = 0.0
28
+ aleatoric_var: float = 0.0
29
+ n_effective: float = 1.0
30
+ extra: dict[str, Any] = field(default_factory=dict)
31
+
32
+ def __post_init__(self) -> None:
33
+ if self.kind == "beta" and len(self.params) != 2:
34
+ raise ValueError("beta uncertainty needs (alpha, beta)")
35
+ if self.kind == "gauss" and len(self.params) != 2:
36
+ raise ValueError("gauss uncertainty needs (mu, sigma_squared)")
37
+ if self.kind == "dirichlet" and len(self.params) < 2:
38
+ raise ValueError("dirichlet uncertainty needs ≥2 alphas")
39
+ if self.epistemic_var < 0 or self.aleatoric_var < 0:
40
+ raise ValueError("variances must be non-negative")
41
+ if self.n_effective <= 0:
42
+ raise ValueError("n_effective must be positive")
43
+
44
+ # ---- constructors ----------------------------------------------------
45
+
46
+ @classmethod
47
+ def beta(cls, alpha: float, beta: float, *, n_eff: float | None = None) -> Uncertainty:
48
+ if alpha <= 0 or beta <= 0:
49
+ raise ValueError(
50
+ f"beta params must be positive (got alpha={alpha}, beta={beta}).\n"
51
+ f" Hint: use Uncertainty.unknown() if you have no prior, or "
52
+ f"alpha=successes+1, beta=failures+1 for Laplace-smoothed counts."
53
+ )
54
+ n = n_eff if n_eff is not None else alpha + beta
55
+ var = (alpha * beta) / ((alpha + beta) ** 2 * (alpha + beta + 1))
56
+ return cls(
57
+ kind="beta",
58
+ params=(float(alpha), float(beta)),
59
+ epistemic_var=var,
60
+ aleatoric_var=0.0,
61
+ n_effective=float(n),
62
+ )
63
+
64
+ @classmethod
65
+ def unknown(cls) -> Uncertainty:
66
+ """The 'I have no prior' default — Beta(1, 1), uniform on [0, 1].
67
+
68
+ Use this when publishing a claim before any evidence has been
69
+ gathered. The reputation kernel treats this as a non-informative
70
+ report; oracle resolution will move the posterior decisively.
71
+ """
72
+ return cls.beta(1, 1)
73
+
74
+ @classmethod
75
+ def point(cls, p: float, *, n_eff: float = 1.0) -> Uncertainty:
76
+ """A near-delta posterior at probability `p` ∈ [0, 1].
77
+
78
+ Use this for confident-but-fallible point claims. Internally
79
+ encodes as Beta(α, β) with α + β = n_eff and mean ≈ p.
80
+ """
81
+ if not 0.0 <= p <= 1.0:
82
+ raise ValueError(
83
+ f"point probability must be in [0, 1] (got {p}).\n"
84
+ f" Hint: Uncertainty.point(p) takes a probability, not log-odds."
85
+ )
86
+ # Tiny offsets keep params strictly positive while the mean stays at p.
87
+ alpha = max(p * n_eff, 1e-3)
88
+ beta = max((1 - p) * n_eff, 1e-3)
89
+ return cls.beta(alpha, beta, n_eff=n_eff)
90
+
91
+ @classmethod
92
+ def dirichlet(cls, alphas: tuple[float, ...]) -> Uncertainty:
93
+ alphas = tuple(float(a) for a in alphas)
94
+ if any(a <= 0 for a in alphas):
95
+ raise ValueError("dirichlet alphas must be positive")
96
+ a0 = sum(alphas)
97
+ # variance of marginal = α_i * (a0 - α_i) / (a0² * (a0 + 1))
98
+ var = sum(a * (a0 - a) / (a0**2 * (a0 + 1)) for a in alphas) / len(alphas)
99
+ return cls(
100
+ kind="dirichlet",
101
+ params=alphas,
102
+ epistemic_var=var,
103
+ aleatoric_var=0.0,
104
+ n_effective=a0,
105
+ )
106
+
107
+ @classmethod
108
+ def gauss(cls, mu: float, sigma2: float, *, n_eff: float = 1.0) -> Uncertainty:
109
+ if sigma2 < 0:
110
+ raise ValueError("gauss variance must be non-negative")
111
+ return cls(
112
+ kind="gauss",
113
+ params=(float(mu), float(sigma2)),
114
+ epistemic_var=float(sigma2),
115
+ aleatoric_var=0.0,
116
+ n_effective=float(n_eff),
117
+ )
118
+
119
+ @classmethod
120
+ def from_logits(
121
+ cls,
122
+ logits: list[float],
123
+ *,
124
+ temperature: float = 1.0,
125
+ deep_ensemble: list[list[float]] | None = None,
126
+ ) -> Uncertainty:
127
+ """Convert classifier logits to a Dirichlet posterior.
128
+
129
+ If `deep_ensemble` is provided, epistemic_var is the disagreement
130
+ across ensemble members (BALD-style); otherwise epistemic_var is
131
+ the marginal variance and aleatoric_var is set to 0.
132
+ """
133
+ scaled = [logit / temperature for logit in logits]
134
+ m = max(scaled)
135
+ exps = [math.exp(x - m) for x in scaled]
136
+ z = sum(exps)
137
+ probs = [e / z for e in exps]
138
+ # Pseudo-counts: one effective observation per logit dimension.
139
+ alphas = tuple(max(p * len(probs) + 1.0, 0.5) for p in probs)
140
+ u = cls.dirichlet(alphas)
141
+ if deep_ensemble:
142
+ ens_probs = []
143
+ for ens_logits in deep_ensemble:
144
+ m2 = max(ens_logits)
145
+ exps2 = [math.exp(x - m2) for x in ens_logits]
146
+ z2 = sum(exps2)
147
+ ens_probs.append([e / z2 for e in exps2])
148
+ mean_probs = [
149
+ sum(ep[i] for ep in ens_probs) / len(ens_probs) for i in range(len(probs))
150
+ ]
151
+ ens_var = sum(
152
+ sum((ep[i] - mean_probs[i]) ** 2 for ep in ens_probs) / len(ens_probs)
153
+ for i in range(len(probs))
154
+ ) / len(probs)
155
+ u = cls(
156
+ kind=u.kind,
157
+ params=u.params,
158
+ epistemic_var=ens_var,
159
+ aleatoric_var=u.epistemic_var,
160
+ n_effective=u.n_effective,
161
+ )
162
+ return u
163
+
164
+ # ---- scoring ---------------------------------------------------------
165
+
166
+ def mean(self) -> float:
167
+ if self.kind == "beta":
168
+ a, b = self.params
169
+ return a / (a + b)
170
+ if self.kind == "gauss":
171
+ return self.params[0]
172
+ if self.kind == "dirichlet":
173
+ a0 = sum(self.params)
174
+ # Mean of last dimension as the "positive" class (convention).
175
+ return self.params[-1] / a0
176
+ raise ValueError(f"unknown kind {self.kind}")
177
+
178
+ def brier(self, outcome: int) -> float:
179
+ """Brier score against a binary outcome (lower = better)."""
180
+ if outcome not in (0, 1):
181
+ raise ValueError("brier outcome must be 0 or 1")
182
+ return (self.mean() - outcome) ** 2
183
+
184
+ def log_score(self, outcome: int) -> float:
185
+ """Log-loss against a binary outcome (lower = better)."""
186
+ p = self.mean()
187
+ # avoid log(0)
188
+ p = min(max(p, 1e-9), 1 - 1e-9)
189
+ return -math.log(p if outcome == 1 else 1 - p)
190
+
191
+ # ---- wire serialization ----------------------------------------------
192
+
193
+ def __repr__(self) -> str:
194
+ if self.kind == "beta":
195
+ a, b = self.params
196
+ return f"Uncertainty.beta(α={a:g}, β={b:g}, mean={self.mean():.3f}, var={self.epistemic_var:.4f})"
197
+ if self.kind == "gauss":
198
+ mu, s2 = self.params
199
+ return f"Uncertainty.gauss(μ={mu:g}, σ²={s2:.4f}, n_eff={self.n_effective:g})"
200
+ if self.kind == "dirichlet":
201
+ return f"Uncertainty.dirichlet(α={tuple(round(p, 3) for p in self.params)}, n_eff={self.n_effective:g})"
202
+ return f"Uncertainty(kind={self.kind!r}, params={self.params!r})"
203
+
204
+ def to_cbor(self) -> dict:
205
+ """COSE protected header label -65608 payload."""
206
+ return {
207
+ "kind": self.kind,
208
+ "params": list(self.params),
209
+ "epistemic_var": self.epistemic_var,
210
+ "aleatoric_var": self.aleatoric_var,
211
+ "n_effective": self.n_effective,
212
+ }
213
+
214
+ @classmethod
215
+ def from_cbor(cls, m: dict) -> Uncertainty:
216
+ return cls(
217
+ kind=m["kind"],
218
+ params=tuple(m["params"]),
219
+ epistemic_var=float(m.get("epistemic_var", 0.0)),
220
+ aleatoric_var=float(m.get("aleatoric_var", 0.0)),
221
+ n_effective=float(m.get("n_effective", 1.0)),
222
+ )