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,38 @@
1
+ """Hashing primitives — SHA-256 for kids, BLAKE3 for fingerprints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+
7
+ import blake3 as _blake3
8
+
9
+ from hypermind.crypto.domain import DomainTag, domain_separated_hash
10
+
11
+
12
+ def sha256_kid(payload: bytes) -> bytes:
13
+ """Compute the kid (knowledge id) for a payload.
14
+
15
+ `kid = sha256(0x02 || payload)` — domain tag KID enforces that this
16
+ 32-byte digest cannot be confused with any other hash operation in
17
+ the protocol.
18
+ """
19
+ return domain_separated_hash(DomainTag.KID, payload)
20
+
21
+
22
+ def sha256_with_tag(tag: DomainTag, payload: bytes) -> bytes:
23
+ """Generic domain-tagged SHA-256."""
24
+ return domain_separated_hash(tag, payload)
25
+
26
+
27
+ def blake3_32(payload: bytes) -> bytes:
28
+ """32-byte BLAKE3 digest. Used for envelope schema hashes and
29
+ anti-entropy fingerprints (truncation-safe at 64 bits).
30
+ """
31
+ return _blake3.blake3(payload).digest()
32
+
33
+
34
+ def hmac_sha256(key: bytes, msg: bytes) -> bytes:
35
+ """HMAC-SHA-256 — used for keyed-pseudonym salts (HMAC custody)."""
36
+ import hmac
37
+
38
+ return hmac.new(key, msg, hashlib.sha256).digest()
@@ -0,0 +1,163 @@
1
+ """Hybrid Logical Clock (Kulkarni et al. 2014).
2
+
3
+ `HLC = (logical_ms: u64, counter: u32, node_id: bytes[8])`
4
+
5
+ v0.1 normative pin (closes spec/06-conformance.md gap #2):
6
+ - logical_ms = CLOCK_REALTIME snapshot in ms, single read at envelope-validate entry
7
+ - NTP sync ≤500 ms drift required
8
+ - clamp on receive: l_stored = min(l_received, now_ms + hlc_max_skew_ms)
9
+ - reject if l_received > now_ms + 2 × hlc_max_skew_ms (anomaly: clock_drift)
10
+
11
+ Clock-source contract (WS-H finding C5):
12
+ - `ClockSource` Protocol pairs a wall-clock with a monotonic clock.
13
+ - `SystemClock` (the default) detects >100ms divergence between calls and
14
+ emits `ClockSkewWarning`. This is the early-warning signal for NTP step,
15
+ VM pause, or backward leap second — all of which can cause `merge()` to
16
+ reject HLCs as exceeding `2 * HLC_MAX_SKEW_MS`.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import time
22
+ import warnings
23
+ from dataclasses import dataclass, field
24
+ from typing import ClassVar, Protocol, runtime_checkable
25
+
26
+
27
+ class HLCSkewError(Exception):
28
+ """Received HLC exceeds 2× hlc_max_skew_ms — reject, do not clamp."""
29
+
30
+
31
+ class ClockSkewWarning(UserWarning):
32
+ """Emitted when wall-clock and monotonic-clock diverge by more than 100ms between calls."""
33
+
34
+
35
+ @runtime_checkable
36
+ class ClockSource(Protocol):
37
+ """Contract for HLC time sources.
38
+
39
+ Implementations must provide:
40
+ - now_ms(): wall-clock-anchored milliseconds (allowed to jump under NTP step)
41
+ - monotonic_ms(): monotonic milliseconds (must never go backwards)
42
+
43
+ Default impl pairs time.time_ns() with time.monotonic_ns(); detects skew jumps
44
+ >100ms between calls and emits ClockSkewWarning.
45
+ """
46
+
47
+ def now_ms(self) -> int: ...
48
+ def monotonic_ms(self) -> int: ...
49
+
50
+
51
+ class SystemClock:
52
+ """Default ClockSource using time.time_ns() + time.monotonic_ns()."""
53
+
54
+ def __init__(self) -> None:
55
+ self._last_wall: int | None = None
56
+ self._last_mono: int | None = None
57
+
58
+ def now_ms(self) -> int:
59
+ wall = time.time_ns() // 1_000_000
60
+ mono = time.monotonic_ns() // 1_000_000
61
+ if self._last_wall is not None and self._last_mono is not None:
62
+ wall_delta = wall - self._last_wall
63
+ mono_delta = mono - self._last_mono
64
+ jump_ms = abs(wall_delta - mono_delta)
65
+ if jump_ms > 100:
66
+ warnings.warn(
67
+ f"ClockSkewWarning: wall-clock jumped {jump_ms}ms relative to monotonic "
68
+ "(NTP step, VM pause, or backward leap second). HLC operations across "
69
+ "this boundary may be rejected by HLC_MAX_SKEW_MS.",
70
+ ClockSkewWarning,
71
+ stacklevel=2,
72
+ )
73
+ self._last_wall = wall
74
+ self._last_mono = mono
75
+ return wall
76
+
77
+ def monotonic_ms(self) -> int:
78
+ return time.monotonic_ns() // 1_000_000
79
+
80
+
81
+ @dataclass(frozen=True, order=True)
82
+ class HLC:
83
+ """Lexicographically-orderable hybrid logical clock value."""
84
+
85
+ logical_ms: int
86
+ counter: int
87
+ node_id: bytes
88
+ # Non-comparable, non-hashed companion state; `frozen=True` requires object.__setattr__
89
+ # to mutate, which we only do for skew-rejection counting in merge().
90
+ clock: ClockSource = field(default=None, compare=False, repr=False, hash=False) # type: ignore[assignment]
91
+ _skew_rejections: list = field(default_factory=list, compare=False, repr=False, hash=False)
92
+
93
+ HLC_MAX_SKEW_MS: ClassVar[int] = 300_000 # 5 min, normative v0.1
94
+
95
+ def __post_init__(self) -> None:
96
+ if len(self.node_id) != 8:
97
+ raise ValueError("node_id must be 8 bytes")
98
+ if self.logical_ms < 0 or self.counter < 0:
99
+ raise ValueError("HLC components must be non-negative")
100
+ if self.clock is None:
101
+ object.__setattr__(self, "clock", SystemClock())
102
+
103
+ @classmethod
104
+ def now(cls, node_id: bytes, clock: ClockSource | None = None) -> HLC:
105
+ """Snapshot current wall-clock as logical_ms."""
106
+ c = clock if clock is not None else SystemClock()
107
+ return cls(
108
+ logical_ms=c.now_ms(),
109
+ counter=0,
110
+ node_id=node_id[:8].ljust(8, b"\x00"),
111
+ clock=c,
112
+ )
113
+
114
+ def tick(self) -> HLC:
115
+ """Advance for a local event."""
116
+ now_ms = self.clock.now_ms()
117
+ if now_ms > self.logical_ms:
118
+ return HLC(now_ms, 0, self.node_id, clock=self.clock)
119
+ return HLC(self.logical_ms, self.counter + 1, self.node_id, clock=self.clock)
120
+
121
+ def merge(self, received: HLC) -> HLC:
122
+ """Merge a received HLC, applying clamp + skew reject."""
123
+ now_ms = self.clock.now_ms()
124
+ if received.logical_ms > now_ms + 2 * self.HLC_MAX_SKEW_MS:
125
+ # Increment skew-rejection counter (using list as a mutable bucket on a frozen dc).
126
+ self._skew_rejections.append(1)
127
+ raise HLCSkewError(
128
+ f"received HLC {received.logical_ms} exceeds 2× max skew "
129
+ f"({2 * self.HLC_MAX_SKEW_MS}ms) above local now {now_ms}"
130
+ )
131
+ clamped_l = min(received.logical_ms, now_ms + self.HLC_MAX_SKEW_MS)
132
+ new_l = max(self.logical_ms, clamped_l, now_ms)
133
+ if new_l == self.logical_ms == clamped_l:
134
+ new_c = max(self.counter, received.counter) + 1
135
+ elif new_l == self.logical_ms:
136
+ new_c = self.counter + 1
137
+ elif new_l == clamped_l:
138
+ new_c = received.counter + 1
139
+ else:
140
+ new_c = 0
141
+ # Carry the skew-rejection bucket forward so metrics persist across merges.
142
+ merged = HLC(new_l, new_c, self.node_id, clock=self.clock)
143
+ merged._skew_rejections.extend(self._skew_rejections)
144
+ return merged
145
+
146
+ def get_skew_metrics(self) -> dict[str, int]:
147
+ """Return clock-skew telemetry for this HLC chain."""
148
+ return {
149
+ "skew_rejections_total": len(self._skew_rejections),
150
+ "current_logical_ms": self.logical_ms,
151
+ "current_counter": self.counter,
152
+ }
153
+
154
+ def to_cbor(self) -> list:
155
+ """CBOR triple per protected header label -65603."""
156
+ return [self.logical_ms, self.counter, self.node_id]
157
+
158
+ @classmethod
159
+ def from_cbor(cls, triple: list) -> HLC:
160
+ return cls(int(triple[0]), int(triple[1]), bytes(triple[2]))
161
+
162
+ def __repr__(self) -> str:
163
+ return f"HLC({self.logical_ms}.{self.counter}@{self.node_id.hex()[:8]})"
@@ -0,0 +1,277 @@
1
+ """RFC 9180 HPKE (base mode) — DHKEM(X25519, HKDF-SHA256) + AES-256-GCM/ChaCha20-Poly1305.
2
+
3
+ Implements only HPKE base mode (no PSK, no auth), single-shot seal/open.
4
+ Used by L3 envelope encryption (`cose_encrypt.py`) and Room epoch keying.
5
+
6
+ Domain-separation note: a dedicated `DomainTag.HPKE_INFO` byte was considered
7
+ for `crypto/domain.py`, but the existing closed-set tag space (0x01..0x18) is
8
+ guarded by `reject_unallocated`. Adding 0x40 would create a hole and break the
9
+ contiguous-allocation invariant tested in `tests/test_crypto.py`. Caller-supplied
10
+ HPKE `info` strings already provide cross-protocol separation per RFC 9180 §9.6.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import struct
17
+
18
+ from cryptography.hazmat.primitives import hashes, hmac
19
+ from cryptography.hazmat.primitives.asymmetric import x25519
20
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
21
+ from cryptography.hazmat.primitives.serialization import (
22
+ Encoding,
23
+ NoEncryption,
24
+ PrivateFormat,
25
+ PublicFormat,
26
+ )
27
+
28
+ # --------------------------------------------------------------------------- #
29
+ # Suite registry — RFC 9180 §7.
30
+ # --------------------------------------------------------------------------- #
31
+ SUITE_DHKEM_X25519_HKDF_SHA256_AES256GCM = 0x0001
32
+ SUITE_DHKEM_X25519_HKDF_SHA256_CHACHA20_POLY1305 = 0x0002
33
+
34
+ # RFC 9180 §7.1 KEM ID for DHKEM(X25519, HKDF-SHA256)
35
+ _KEM_ID_X25519 = 0x0020
36
+ # RFC 9180 §7.2 KDF ID for HKDF-SHA256
37
+ _KDF_ID_HKDF_SHA256 = 0x0001
38
+ # RFC 9180 §7.3 AEAD IDs
39
+ _AEAD_ID_AES256GCM = 0x0002
40
+ _AEAD_ID_CHACHA20POLY1305 = 0x0003
41
+
42
+ _MODE_BASE = 0x00
43
+
44
+ _NH = 32 # HKDF-SHA256 output length
45
+ _NSECRET = 32 # DHKEM(X25519) shared-secret length
46
+ _NPK = 32 # X25519 public-key length
47
+ _NSK = 32 # X25519 private-key length
48
+
49
+
50
+ def _suite_aead(suite_id: int) -> tuple[int, int]:
51
+ """Return (Nk, Nn) for the suite's AEAD."""
52
+ if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_AES256GCM:
53
+ return 32, 12
54
+ if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_CHACHA20_POLY1305:
55
+ return 32, 12
56
+ raise ValueError(f"unsupported HPKE suite id: 0x{suite_id:04x}")
57
+
58
+
59
+ def _suite_aead_id(suite_id: int) -> int:
60
+ if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_AES256GCM:
61
+ return _AEAD_ID_AES256GCM
62
+ if suite_id == SUITE_DHKEM_X25519_HKDF_SHA256_CHACHA20_POLY1305:
63
+ return _AEAD_ID_CHACHA20POLY1305
64
+ raise ValueError(f"unsupported HPKE suite id: 0x{suite_id:04x}")
65
+
66
+
67
+ def _new_aead(suite_id: int, key: bytes) -> AESGCM | ChaCha20Poly1305:
68
+ aead_id = _suite_aead_id(suite_id)
69
+ if aead_id == _AEAD_ID_AES256GCM:
70
+ return AESGCM(key)
71
+ return ChaCha20Poly1305(key)
72
+
73
+
74
+ # --------------------------------------------------------------------------- #
75
+ # HKDF labelled extract/expand — RFC 9180 §4.1.
76
+ # --------------------------------------------------------------------------- #
77
+ _HPKE_VERSION = b"HPKE-v1"
78
+
79
+
80
+ def _hmac_sha256(key: bytes, data: bytes) -> bytes:
81
+ h = hmac.HMAC(key, hashes.SHA256())
82
+ h.update(data)
83
+ return h.finalize()
84
+
85
+
86
+ def _hkdf_extract(salt: bytes, ikm: bytes) -> bytes:
87
+ if not salt:
88
+ salt = b"\x00" * _NH
89
+ return _hmac_sha256(salt, ikm)
90
+
91
+
92
+ def _hkdf_expand(prk: bytes, info: bytes, length: int) -> bytes:
93
+ n = (length + _NH - 1) // _NH
94
+ if n > 255:
95
+ raise ValueError("HKDF-Expand: requested length too large")
96
+ out = b""
97
+ t = b""
98
+ for i in range(1, n + 1):
99
+ t = _hmac_sha256(prk, t + info + bytes([i]))
100
+ out += t
101
+ return out[:length]
102
+
103
+
104
+ def _suite_id_kem() -> bytes:
105
+ # "KEM" || I2OSP(kem_id, 2)
106
+ return b"KEM" + struct.pack(">H", _KEM_ID_X25519)
107
+
108
+
109
+ def _suite_id_hpke(suite_id: int) -> bytes:
110
+ # "HPKE" || I2OSP(kem_id,2) || I2OSP(kdf_id,2) || I2OSP(aead_id,2)
111
+ return (
112
+ b"HPKE"
113
+ + struct.pack(">H", _KEM_ID_X25519)
114
+ + struct.pack(">H", _KDF_ID_HKDF_SHA256)
115
+ + struct.pack(">H", _suite_aead_id(suite_id))
116
+ )
117
+
118
+
119
+ def _labeled_extract(salt: bytes, suite_id_bytes: bytes, label: bytes, ikm: bytes) -> bytes:
120
+ labeled_ikm = _HPKE_VERSION + suite_id_bytes + label + ikm
121
+ return _hkdf_extract(salt, labeled_ikm)
122
+
123
+
124
+ def _labeled_expand(
125
+ prk: bytes, suite_id_bytes: bytes, label: bytes, info: bytes, length: int
126
+ ) -> bytes:
127
+ labeled_info = struct.pack(">H", length) + _HPKE_VERSION + suite_id_bytes + label + info
128
+ return _hkdf_expand(prk, labeled_info, length)
129
+
130
+
131
+ # --------------------------------------------------------------------------- #
132
+ # DHKEM(X25519, HKDF-SHA256) — RFC 9180 §4.1.
133
+ # --------------------------------------------------------------------------- #
134
+ def _serialize_pk(pk: x25519.X25519PublicKey) -> bytes:
135
+ return pk.public_bytes(Encoding.Raw, PublicFormat.Raw)
136
+
137
+
138
+ def _deserialize_pk(raw: bytes) -> x25519.X25519PublicKey:
139
+ return x25519.X25519PublicKey.from_public_bytes(raw)
140
+
141
+
142
+ def _serialize_sk(sk: x25519.X25519PrivateKey) -> bytes:
143
+ return sk.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
144
+
145
+
146
+ def _deserialize_sk(raw: bytes) -> x25519.X25519PrivateKey:
147
+ return x25519.X25519PrivateKey.from_private_bytes(raw)
148
+
149
+
150
+ def _extract_and_expand(dh: bytes, kem_context: bytes) -> bytes:
151
+ suite_id = _suite_id_kem()
152
+ eae_prk = _labeled_extract(b"", suite_id, b"eae_prk", dh)
153
+ return _labeled_expand(eae_prk, suite_id, b"shared_secret", kem_context, _NSECRET)
154
+
155
+
156
+ def _kem_encap(
157
+ recipient_pub: bytes, eph_priv: x25519.X25519PrivateKey | None = None
158
+ ) -> tuple[bytes, bytes]:
159
+ """DHKEM Encap. Returns (shared_secret, enc)."""
160
+ pk_r = _deserialize_pk(recipient_pub)
161
+ if eph_priv is None:
162
+ eph_priv = x25519.X25519PrivateKey.generate()
163
+ enc = _serialize_pk(eph_priv.public_key())
164
+ dh = eph_priv.exchange(pk_r)
165
+ kem_context = enc + recipient_pub
166
+ shared_secret = _extract_and_expand(dh, kem_context)
167
+ return shared_secret, enc
168
+
169
+
170
+ def _kem_decap(recipient_priv: bytes, enc: bytes) -> bytes:
171
+ sk_r = _deserialize_sk(recipient_priv)
172
+ pk_e = _deserialize_pk(enc)
173
+ dh = sk_r.exchange(pk_e)
174
+ pk_r_bytes = _serialize_pk(sk_r.public_key())
175
+ kem_context = enc + pk_r_bytes
176
+ return _extract_and_expand(dh, kem_context)
177
+
178
+
179
+ # --------------------------------------------------------------------------- #
180
+ # Key schedule (base mode) — RFC 9180 §5.1.
181
+ # --------------------------------------------------------------------------- #
182
+ def _key_schedule_base(suite_id: int, shared_secret: bytes, info: bytes) -> tuple[bytes, bytes]:
183
+ """Return (key, base_nonce) for base-mode single-shot AEAD."""
184
+ suite_bytes = _suite_id_hpke(suite_id)
185
+ psk_id_hash = _labeled_extract(b"", suite_bytes, b"psk_id_hash", b"")
186
+ info_hash = _labeled_extract(b"", suite_bytes, b"info_hash", info)
187
+ key_schedule_context = bytes([_MODE_BASE]) + psk_id_hash + info_hash
188
+
189
+ secret = _labeled_extract(shared_secret, suite_bytes, b"secret", b"")
190
+ nk, nn = _suite_aead(suite_id)
191
+ key = _labeled_expand(secret, suite_bytes, b"key", key_schedule_context, nk)
192
+ base_nonce = _labeled_expand(secret, suite_bytes, b"base_nonce", key_schedule_context, nn)
193
+ return key, base_nonce
194
+
195
+
196
+ # --------------------------------------------------------------------------- #
197
+ # Public API.
198
+ # --------------------------------------------------------------------------- #
199
+ def seal(
200
+ suite_id: int,
201
+ recipient_pub: bytes,
202
+ info: bytes,
203
+ aad: bytes,
204
+ plaintext: bytes,
205
+ *,
206
+ _test_eph_priv: bytes | None = None,
207
+ ) -> tuple[bytes, bytes]:
208
+ """HPKE base-mode single-shot seal.
209
+
210
+ Args:
211
+ suite_id: One of the SUITE_* constants in this module.
212
+ recipient_pub: Recipient X25519 public key (32 bytes).
213
+ info: Application-bound contextual string per RFC 9180 §5.1.
214
+ aad: Additional authenticated data (bound by AEAD).
215
+ plaintext: Bytes to encrypt.
216
+ _test_eph_priv: Test-only override for the ephemeral private key. Production
217
+ callers MUST omit this; a fresh random ephemeral is generated per call.
218
+
219
+ Returns:
220
+ Tuple `(enc, ciphertext)`. `enc` is the encapsulated key (32 bytes for
221
+ X25519). `ciphertext` includes the 16-byte AEAD tag.
222
+ """
223
+ if len(recipient_pub) != _NPK:
224
+ raise ValueError(f"recipient_pub must be {_NPK} bytes")
225
+ eph = _deserialize_sk(_test_eph_priv) if _test_eph_priv is not None else None
226
+ shared_secret, enc = _kem_encap(recipient_pub, eph_priv=eph)
227
+ key, base_nonce = _key_schedule_base(suite_id, shared_secret, info)
228
+ ct = _new_aead(suite_id, key).encrypt(base_nonce, plaintext, aad)
229
+ return enc, ct
230
+
231
+
232
+ def open(
233
+ suite_id: int,
234
+ recipient_priv: bytes,
235
+ enc: bytes,
236
+ info: bytes,
237
+ aad: bytes,
238
+ ciphertext: bytes,
239
+ ) -> bytes:
240
+ """HPKE base-mode single-shot open.
241
+
242
+ Args:
243
+ suite_id: One of the SUITE_* constants in this module.
244
+ recipient_priv: Recipient X25519 private key (32 bytes).
245
+ enc: Encapsulated key from `seal()`.
246
+ info: Same `info` passed to `seal()`.
247
+ aad: Same `aad` passed to `seal()`.
248
+ ciphertext: Output of `seal()`.
249
+
250
+ Returns:
251
+ Decrypted plaintext bytes.
252
+
253
+ Raises:
254
+ cryptography.exceptions.InvalidTag: AEAD verification failed.
255
+ """
256
+ if len(recipient_priv) != _NSK:
257
+ raise ValueError(f"recipient_priv must be {_NSK} bytes")
258
+ if len(enc) != _NPK:
259
+ raise ValueError(f"enc must be {_NPK} bytes")
260
+ shared_secret = _kem_decap(recipient_priv, enc)
261
+ key, base_nonce = _key_schedule_base(suite_id, shared_secret, info)
262
+ return _new_aead(suite_id, key).decrypt(base_nonce, ciphertext, aad)
263
+
264
+
265
+ def generate_keypair() -> tuple[bytes, bytes]:
266
+ """Generate a fresh X25519 keypair. Returns `(private_raw, public_raw)`."""
267
+ sk = x25519.X25519PrivateKey.generate()
268
+ return _serialize_sk(sk), _serialize_pk(sk.public_key())
269
+
270
+
271
+ # Pure-Python fallback gate (RFC 9180 implementations exist but the project pins
272
+ # `cryptography>=44`; any host missing native primitives can opt-in here).
273
+ if os.environ.get("HM_PURE_PY_HPKE") == "1": # pragma: no cover - opt-in path
274
+ raise RuntimeError(
275
+ "HM_PURE_PY_HPKE=1 set but no pure-Python HPKE backend is bundled. "
276
+ "Install cryptography>=44 or remove the env var."
277
+ )
@@ -0,0 +1,196 @@
1
+ """Signer abstraction — pluggable signing backends for v0.8 production profile.
2
+
3
+ The SDK historically embedded `Keypair` directly in the agent. That is fine
4
+ for sandbox / single-tenant deployments but blocks the v0.8 production
5
+ profile where signing keys are expected to live in:
6
+
7
+ * AWS KMS — keys created with `KeySpec=ECC_NIST_P256` cannot be used
8
+ directly (Ed25519 is not yet GA in KMS as of 2026-04-27); the planned
9
+ backend wraps a customer-supplied `Sign` API call and validates the
10
+ returned DER signature against the published kid.
11
+ * GCP Cloud KMS — `EC_SIGN_ED25519` algorithm; the backend issues an
12
+ `AsymmetricSign` RPC and unwraps the IETF-encoded signature.
13
+ * HashiCorp Vault Transit — Ed25519 keys via the `transit/sign/<key>`
14
+ endpoint; backend uses the leased AppRole token, refreshes via the
15
+ sidecar.
16
+
17
+ All three back-ends are wired in v0.8.5 (see ROADMAP). v0.8.0 ships
18
+ the `Signer` Protocol and the `LocalSigner` adapter so the rest of the
19
+ SDK can already program against the abstraction.
20
+
21
+ Security invariants (production profile):
22
+
23
+ * `kid` is always the 32-byte Ed25519 public key half — never the secret.
24
+ This matches the wire format's `COSE_KID` discriminator.
25
+ * `sign(msg)` MUST return a raw signature (no envelope, no DER wrapper)
26
+ matching `crypto.signing.verify_signature(alg, public_key, sig, msg)`.
27
+ * The Signer MUST refuse `sign()` after the underlying key material has
28
+ been rotated; callers detect this via a stable `kid` plus monotonic
29
+ rotation epoch.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import Protocol, runtime_checkable
35
+
36
+ from hypermind.crypto.signing import ALG_ED25519, Keypair
37
+
38
+
39
+ @runtime_checkable
40
+ class Signer(Protocol):
41
+ """Pluggable signing backend.
42
+
43
+ Implementations MUST be safe to call concurrently from multiple
44
+ coroutines; the production AWS/GCP/Vault adapters serialise inside
45
+ the SDK they wrap.
46
+ """
47
+
48
+ @property
49
+ def public_key(self) -> bytes:
50
+ """Full algorithm-specific public-key material.
51
+
52
+ For Ed25519: the 32-byte raw public key.
53
+ For composite (alg=-49): `ed_pk(32) || mldsa_pk(1952)`.
54
+ """
55
+ ...
56
+
57
+ @property
58
+ def kid(self) -> bytes:
59
+ """Stable 32-byte agent identity (Ed25519 public-key half)."""
60
+ ...
61
+
62
+ @property
63
+ def alg(self) -> int:
64
+ """COSE algorithm identifier (e.g. ALG_ED25519 = -8)."""
65
+ ...
66
+
67
+ def sign(self, message: bytes) -> bytes:
68
+ """Return a raw signature over `message` (no envelope)."""
69
+ ...
70
+
71
+
72
+ class LocalSigner:
73
+ """In-process Signer backed by a `Keypair`.
74
+
75
+ This is the v0.1–v0.7 path: the secret is held in memory by the
76
+ agent. Suitable for sandbox/testbed and for self-hosted deployments
77
+ where operators have decided that an HSM/KMS dependency is not
78
+ required.
79
+
80
+ The constructor accepts either a `Keypair` (preferred) or a 32-byte
81
+ Ed25519 seed for deterministic test fixtures.
82
+ """
83
+
84
+ def __init__(self, keypair: Keypair) -> None:
85
+ self._kp = keypair
86
+
87
+ @classmethod
88
+ def from_seed(cls, seed: bytes) -> LocalSigner:
89
+ return cls(Keypair.from_seed(seed))
90
+
91
+ @classmethod
92
+ def generate(cls, *, alg: int = ALG_ED25519) -> LocalSigner:
93
+ return cls(Keypair.generate(alg=alg))
94
+
95
+ @property
96
+ def keypair(self) -> Keypair:
97
+ """Escape hatch for code paths that still need the raw Keypair.
98
+
99
+ Removed in v0.9; callers should switch to `sign()` and `kid`.
100
+ """
101
+ return self._kp
102
+
103
+ @property
104
+ def public_key(self) -> bytes:
105
+ return self._kp.public_bytes
106
+
107
+ @property
108
+ def kid(self) -> bytes:
109
+ return self._kp.kid
110
+
111
+ @property
112
+ def alg(self) -> int:
113
+ return self._kp.alg
114
+
115
+ def sign(self, message: bytes) -> bytes:
116
+ return self._kp.sign(message)
117
+
118
+ def __repr__(self) -> str:
119
+ return f"LocalSigner(kid={self.kid.hex()[:16]}..., alg={self.alg})"
120
+
121
+
122
+ class KMSSigner:
123
+ """Stub Signer for cloud KMS / Vault back-ends.
124
+
125
+ **Not yet wired** — instantiating and calling `sign()` raises
126
+ `NotImplementedError`. The class exists so downstream code can
127
+ program against the contract today and the ROADMAP 0.8.5 backends
128
+ can drop in without an API break.
129
+
130
+ Construction parameters (anticipated):
131
+
132
+ * `provider` — one of `"aws-kms"`, `"gcp-kms"`, `"vault-transit"`.
133
+ * `key_uri` — provider-native key reference. Examples:
134
+ - AWS: ``arn:aws:kms:us-east-1:123456789012:key/abcd-…``
135
+ - GCP: ``projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1``
136
+ - Vault: ``transit/keys/<key-name>``
137
+ * `published_kid` — the 32-byte Ed25519 public key half, fetched
138
+ out-of-band from the provider's `GetPublicKey` API and pinned at
139
+ construction. The Signer refuses to sign if the provider rotates
140
+ the underlying material without the operator updating this pin.
141
+ * `auth` — provider auth context (boto3 session, GCP credentials,
142
+ Vault token). Held by reference; the SDK does not persist it.
143
+ """
144
+
145
+ def __init__(
146
+ self,
147
+ *,
148
+ provider: str,
149
+ key_uri: str,
150
+ published_kid: bytes,
151
+ auth: object | None = None,
152
+ alg: int = ALG_ED25519,
153
+ ) -> None:
154
+ if len(published_kid) != 32:
155
+ raise ValueError("published_kid must be 32 bytes (Ed25519 public)")
156
+ if provider not in ("aws-kms", "gcp-kms", "vault-transit"):
157
+ raise ValueError(
158
+ f"unknown KMS provider {provider!r}; expected aws-kms | gcp-kms | vault-transit"
159
+ )
160
+ self._provider = provider
161
+ self._key_uri = key_uri
162
+ self._kid = published_kid
163
+ self._auth = auth
164
+ self._alg = alg
165
+
166
+ @property
167
+ def public_key(self) -> bytes:
168
+ # Today we only know the Ed25519 half (the published kid). When
169
+ # the v0.8.5 backends land, the constructor will fetch the full
170
+ # public-key bytes (composite case includes the ML-DSA-65 half)
171
+ # via the provider's GetPublicKey RPC.
172
+ return self._kid
173
+
174
+ @property
175
+ def kid(self) -> bytes:
176
+ return self._kid
177
+
178
+ @property
179
+ def alg(self) -> int:
180
+ return self._alg
181
+
182
+ def sign(self, message: bytes) -> bytes:
183
+ raise NotImplementedError(
184
+ "KMS backend not yet wired; see ROADMAP 0.8.5. "
185
+ f"Provider={self._provider!r}, key_uri={self._key_uri!r}. "
186
+ "Use LocalSigner for now."
187
+ )
188
+
189
+ def __repr__(self) -> str:
190
+ return (
191
+ f"KMSSigner(provider={self._provider!r}, "
192
+ f"kid={self._kid.hex()[:16]}..., alg={self._alg})"
193
+ )
194
+
195
+
196
+ __all__ = ["KMSSigner", "LocalSigner", "Signer"]