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
hmctl/cli.py ADDED
@@ -0,0 +1,947 @@
1
+ """hmctl entry point.
2
+
3
+ Subcommands map 1:1 onto the SDK's DX surface (introspect / why-blocked
4
+ / discover-mentors / request-vouch / sandbox / audit / inspect / doctor).
5
+
6
+ Run `hmctl --help` for the full subcommand list.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import sys
14
+
15
+ import click
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+
19
+ from hypermind import (
20
+ HyperMindAgent,
21
+ Uncertainty,
22
+ __protocol_version__,
23
+ __version__,
24
+ __wire_version__,
25
+ )
26
+ from hypermind.crypto.signing import (
27
+ ALG_ED25519,
28
+ ALG_MLDSA65_ED25519,
29
+ Keypair,
30
+ )
31
+ from hypermind.observability.recorder import get_recorder
32
+
33
+ console = Console()
34
+
35
+
36
+ @click.group()
37
+ @click.version_option(__version__, prog_name="hmctl")
38
+ def main() -> None:
39
+ """HyperMind conformance CLI."""
40
+
41
+
42
+ @main.command()
43
+ @click.option(
44
+ "--check",
45
+ is_flag=True,
46
+ default=False,
47
+ help="Print SDK version, wire_version, and protocol_version (ROADMAP §0.5.10).",
48
+ )
49
+ def version(check: bool) -> None:
50
+ """Print SDK + wire-version (and protocol_version with --check)."""
51
+ if check:
52
+ click.echo(
53
+ json.dumps(
54
+ {
55
+ "sdk_version": __version__,
56
+ "wire_version": __wire_version__,
57
+ "protocol_version": __protocol_version__,
58
+ },
59
+ indent=2,
60
+ )
61
+ )
62
+ return
63
+ click.echo(f"hmctl {__version__} (wire-version {__wire_version__})")
64
+
65
+
66
+ @main.group()
67
+ def migrate() -> None:
68
+ """Source-codemod helpers (ROADMAP §0.5.9).
69
+
70
+ Subcommands print migration suggestions for a v0.4-style codebase
71
+ moving to v0.5. The codemod is libcst-based and currently EMITS
72
+ SUGGESTIONS only — it does not rewrite source. Install with::
73
+
74
+ pip install 'hypermind[migrate]'
75
+ """
76
+
77
+
78
+ @migrate.command("v0.4-to-v0.5")
79
+ @click.argument("path", type=click.Path(exists=True), default=".")
80
+ def migrate_v04_to_v05(path: str) -> None:
81
+ """Suggest v0.4 → v0.5 migrations under PATH (read-only).
82
+
83
+ Walks every ``*.py`` under PATH and reports:
84
+
85
+ * imports of ``ClaimType``, ``Subscription``, ``ClaimEvent`` etc.
86
+ that should move to ``hypermind.types`` (ROADMAP §0.5.6).
87
+ * calls to ``HyperMindAgent.testbed(...)`` that should become
88
+ ``async with testbed_cohort(...) as cohort:`` (ROADMAP §0.5.4).
89
+ * calls to ``HyperMindAgent.from_env(...)`` that should become
90
+ ``await HyperMindAgent.open(mode=...)`` (already deprecated).
91
+
92
+ libcst-based; no rewrites are applied — review the suggestions and
93
+ apply them by hand. The full rewriter ships in v0.7 (WS-F).
94
+ """
95
+ try:
96
+ import libcst as cst # type: ignore[import-not-found]
97
+ except ImportError:
98
+ click.echo(
99
+ "error: libcst is required for `hmctl migrate`. Install with:\n"
100
+ " pip install 'hypermind[migrate]'",
101
+ err=True,
102
+ )
103
+ raise SystemExit(2) from None
104
+
105
+ import pathlib
106
+
107
+ relocated = {
108
+ "ArgumentDAG",
109
+ "ArgumentEdge",
110
+ "BlockReason",
111
+ "CalibTruthSnapshot",
112
+ "ClaimEvent",
113
+ "ClaimType",
114
+ "DisputeStatus",
115
+ "Kid",
116
+ "PanelMember",
117
+ "PinPolicy",
118
+ "RouteStrategy",
119
+ "SignedStatement",
120
+ "Subscription",
121
+ "VouchRequest",
122
+ }
123
+
124
+ class _SuggestionVisitor(cst.CSTVisitor):
125
+ def __init__(self) -> None:
126
+ self.suggestions: list[str] = []
127
+
128
+ def visit_ImportFrom(self, node: cst.ImportFrom) -> None:
129
+ mod = node.module
130
+ if mod is None or not isinstance(mod, cst.Name) or mod.value != "hypermind":
131
+ return
132
+ if not isinstance(node.names, (list, tuple)):
133
+ return
134
+ for alias in node.names:
135
+ name = alias.name
136
+ if isinstance(name, cst.Name):
137
+ if name.value in relocated:
138
+ self.suggestions.append(
139
+ f"`from hypermind import {name.value}` → "
140
+ f"`from hypermind.types import {name.value}`"
141
+ )
142
+ elif name.value == "testbed_cohort":
143
+ self.suggestions.append(
144
+ "`from hypermind import testbed_cohort` → "
145
+ "`from hypermind.testing import testbed_cohort`"
146
+ )
147
+
148
+ def visit_Call(self, node: cst.Call) -> None:
149
+ func = node.func
150
+ if isinstance(func, cst.Attribute) and isinstance(func.attr, cst.Name):
151
+ if func.attr.value == "testbed":
152
+ self.suggestions.append(
153
+ "`HyperMindAgent.testbed(n)` → "
154
+ "`async with testbed_cohort(n) as agents:` "
155
+ "(from hypermind.testing)"
156
+ )
157
+ if func.attr.value == "from_env":
158
+ self.suggestions.append(
159
+ "`HyperMindAgent.from_env(...)` → `await HyperMindAgent.open(mode=...)`"
160
+ )
161
+
162
+ root = pathlib.Path(path)
163
+ files = list(root.rglob("*.py"))
164
+ if not files:
165
+ click.echo(f"no .py files under {path!r}")
166
+ return
167
+ total = 0
168
+ for f in files:
169
+ try:
170
+ tree = cst.parse_module(f.read_text())
171
+ except Exception as e: # pragma: no cover
172
+ click.echo(f" ! could not parse {f}: {e}", err=True)
173
+ continue
174
+ v = _SuggestionVisitor()
175
+ tree.visit(v)
176
+ if v.suggestions:
177
+ click.echo(str(f))
178
+ for msg in v.suggestions:
179
+ click.echo(f" suggest: {msg}")
180
+ total += len(v.suggestions)
181
+ click.echo(f"\n{total} suggestion(s) across {len(files)} file(s).")
182
+
183
+
184
+ @main.group()
185
+ def key() -> None:
186
+ """Key management."""
187
+
188
+
189
+ @key.command("new")
190
+ @click.option(
191
+ "--hybrid/--no-hybrid",
192
+ default=True,
193
+ help=(
194
+ "Generate a Composite Ed25519 + ML-DSA-65 keypair (v0.7 default). "
195
+ "Use --no-hybrid (or --legacy-ed25519) to fall back to classical Ed25519; "
196
+ "the classical-only path emits a deprecation warning."
197
+ ),
198
+ )
199
+ @click.option(
200
+ "--legacy-ed25519",
201
+ is_flag=True,
202
+ default=False,
203
+ help=(
204
+ "Force generation of a classical-only Ed25519 keypair. v0.7 marks "
205
+ "classical-only as the legacy path; prefer --hybrid (default)."
206
+ ),
207
+ )
208
+ def key_new(hybrid: bool, legacy_ed25519: bool) -> None:
209
+ """Generate a new keypair (Composite hybrid by default in v0.7)."""
210
+ if legacy_ed25519:
211
+ hybrid = False
212
+ if not hybrid:
213
+ click.echo(
214
+ "warning: classical-only signatures will be the legacy path from "
215
+ "v0.7; use --hybrid (default) for the PQ-secure Composite key.",
216
+ err=True,
217
+ )
218
+ kp = Keypair.generate(alg=ALG_ED25519)
219
+ else:
220
+ kp = Keypair.generate(alg=ALG_MLDSA65_ED25519)
221
+ output = {
222
+ "did_key": kp.did_key,
223
+ "public_hex": kp.public_bytes.hex(),
224
+ "alg": kp.alg,
225
+ "alg_label": "composite-mldsa65-ed25519" if kp.alg == ALG_MLDSA65_ED25519 else "ed25519",
226
+ }
227
+ # Only emit secret_hex when stdout is an interactive terminal.
228
+ # Non-TTY callers (pipes, scripts) receive public fields only — the
229
+ # secret never lands in logs or CI artifacts.
230
+ if sys.stdout.isatty():
231
+ output["secret_hex"] = kp.secret_bytes.hex()
232
+ click.echo(json.dumps(output, indent=2))
233
+ click.echo(
234
+ click.style(
235
+ "WARNING: secret key printed above — store it securely and do not share.",
236
+ fg="yellow",
237
+ bold=True,
238
+ ),
239
+ err=True,
240
+ )
241
+ else:
242
+ # Machine-readable path: emit only public fields; no warning noise on stderr.
243
+ click.echo(json.dumps(output, indent=2))
244
+
245
+
246
+ @main.group()
247
+ def sandbox() -> None:
248
+ """Sandbox / testbed commands."""
249
+
250
+
251
+ @sandbox.command("cohort")
252
+ @click.option("--room", default="ai-safety-evals")
253
+ @click.option("--size", default=5)
254
+ def sandbox_cohort(room: str, size: int) -> None:
255
+ """Spin up a pre-vouched testbed cohort."""
256
+ agents = HyperMindAgent.testbed(agents=size, room=room)
257
+ table = Table(title=f"Sandbox cohort — room={room}")
258
+ table.add_column("did:key")
259
+ table.add_column("ramp")
260
+ for a in agents:
261
+ intros = asyncio.run(a.introspect())
262
+ table.add_row(intros["did_key"], "complete" if intros["ramp"]["complete"] else "pending")
263
+ console.print(table)
264
+
265
+
266
+ @main.command()
267
+ def introspect() -> None:
268
+ """Print introspection from a fresh sandbox agent (demo)."""
269
+ agent = HyperMindAgent.testbed(agents=1)[0]
270
+ click.echo(json.dumps(asyncio.run(agent.introspect()), indent=2))
271
+
272
+
273
+ @main.command("why-blocked")
274
+ @click.argument("action")
275
+ def why_blocked(action: str) -> None:
276
+ """Show the BlockReason for ACTION (rule_id + spec_ref)."""
277
+ agent = HyperMindAgent.testbed(agents=1, ramp_complete=False)[0]
278
+ reason = asyncio.run(agent.why_blocked(action))
279
+ click.echo(
280
+ json.dumps(
281
+ {
282
+ "rule_id": reason.rule_id,
283
+ "spec_ref": reason.spec_ref,
284
+ "human_explanation": reason.human_explanation,
285
+ "current_value": reason.current_value,
286
+ "required_value": reason.required_value,
287
+ "retry_after_seconds": reason.retry_after_seconds,
288
+ },
289
+ indent=2,
290
+ )
291
+ )
292
+
293
+
294
+ @main.command()
295
+ @click.option("--since", default="", help="HLC string; only emit records after this.")
296
+ def audit(since: str) -> None:
297
+ """Export the in-process recorder as signed JSONL.
298
+
299
+ In production this would emit the agent's structured audit log;
300
+ sandbox emits the global recorder for testing.
301
+ """
302
+ rec = get_recorder()
303
+ for record in rec.records:
304
+ if since and record.ts_hlc and record.ts_hlc < since:
305
+ continue
306
+ click.echo(
307
+ json.dumps(
308
+ {
309
+ "ts_wall_ms": record.ts_wall_ms,
310
+ "ts_hlc": record.ts_hlc,
311
+ "namespace": record.namespace,
312
+ "agent_id": record.agent_id,
313
+ "event_type": record.event_type,
314
+ "severity": record.severity,
315
+ **record.fields,
316
+ }
317
+ )
318
+ )
319
+
320
+
321
+ @main.command()
322
+ @click.option(
323
+ "--check-clock",
324
+ is_flag=True,
325
+ default=False,
326
+ help="Run the clock-source contract self-check (WS-H finding C5).",
327
+ )
328
+ @click.option(
329
+ "--strict-pq",
330
+ is_flag=True,
331
+ default=False,
332
+ help=(
333
+ "Refuse to pass any agent still on classical-only Ed25519 signatures. "
334
+ "Exits non-zero (rule_id HM-MIN-ALG-001) when the smoke cohort or any "
335
+ "supplied agent fails the v0.7 PQ floor."
336
+ ),
337
+ )
338
+ def doctor(check_clock: bool, strict_pq: bool) -> None:
339
+ """Self-check: verify SDK invariants on a smoke-test cohort."""
340
+ import sys
341
+
342
+ from hypermind.dispute import Dispute, DisputeStatus # noqa: F401 (smoke import)
343
+
344
+ if check_clock:
345
+ _doctor_check_clock()
346
+ return
347
+
348
+ agents = HyperMindAgent.testbed(agents=3, room="t")
349
+ alice, bob, _carol = agents
350
+
351
+ if strict_pq:
352
+ offenders = [a.keypair.did_key for a in agents if a.keypair.alg != ALG_MLDSA65_ED25519]
353
+ if offenders:
354
+ click.echo(
355
+ json.dumps(
356
+ {
357
+ "rule_id": "HM-MIN-ALG-001",
358
+ "spec_ref": (
359
+ "draft-hypermind-scitt-contested-claims"
360
+ " §3.7 (min_signature_alg)"
361
+ ),
362
+ "violation": "classical-only signatures detected",
363
+ "offenders": offenders,
364
+ "hint": (
365
+ "rotate to Composite Ed25519 + ML-DSA-65 via "
366
+ "`hmctl key new --hybrid` (v0.7 default)."
367
+ ),
368
+ },
369
+ indent=2,
370
+ ),
371
+ err=True,
372
+ )
373
+ sys.exit(2)
374
+
375
+ async def _smoke() -> dict:
376
+ kid = await alice.publish_claim(
377
+ {"x": 1},
378
+ claim_type="custom",
379
+ uncertainty=Uncertainty.beta(2, 3),
380
+ )
381
+ d = await bob.dispute(kid, reason="smoke", bond_topic_scope="t")
382
+ return {"sealed_kid": kid.hex(), "dispute_kid": d.hex()}
383
+
384
+ result = asyncio.run(_smoke())
385
+ table = Table(title="hmctl doctor — smoke test passed")
386
+ for k, v in result.items():
387
+ table.add_row(k, v)
388
+ console.print(table)
389
+
390
+
391
+ def _doctor_check_clock() -> None:
392
+ """Print a 3-line wall/monotonic/NTP-offset report for operators."""
393
+ import platform
394
+ import subprocess
395
+ import time as _time
396
+
397
+ from hypermind.crypto.hlc import SystemClock
398
+
399
+ clk = SystemClock()
400
+ mono_a = clk.monotonic_ms()
401
+ _time.sleep(0.005)
402
+ wall_b = clk.now_ms()
403
+ mono_b = clk.monotonic_ms()
404
+ monotonic_ok = mono_b >= mono_a
405
+
406
+ ntp_offset_ms: float | None = None
407
+ ntp_stratum: int | None = None
408
+ ntp_note = "unknown"
409
+ if platform.system() == "Linux":
410
+ try:
411
+ proc = subprocess.run(
412
+ ["chronyc", "tracking"],
413
+ capture_output=True,
414
+ text=True,
415
+ timeout=5,
416
+ )
417
+ if proc.returncode == 0:
418
+ for line in proc.stdout.splitlines():
419
+ if line.lower().startswith("system time"):
420
+ # "System time : 0.000123456 seconds slow of NTP time"
421
+ parts = line.split(":", 1)[1].strip().split()
422
+ if parts:
423
+ try:
424
+ ntp_offset_ms = float(parts[0]) * 1000.0
425
+ except ValueError:
426
+ pass
427
+ elif line.lower().startswith("stratum"):
428
+ parts = line.split(":", 1)
429
+ if len(parts) == 2:
430
+ try:
431
+ ntp_stratum = int(parts[1].strip())
432
+ except ValueError:
433
+ pass
434
+ ntp_note = "ok"
435
+ if ntp_offset_ms is not None and abs(ntp_offset_ms) > 50:
436
+ ntp_note = f"WARNING: offset {ntp_offset_ms:.1f}ms > 50ms"
437
+ if ntp_stratum is not None and ntp_stratum > 3:
438
+ ntp_note = f"WARNING: stratum {ntp_stratum} > 3"
439
+ else:
440
+ ntp_note = "chronyc-error"
441
+ except FileNotFoundError:
442
+ ntp_note = "WARNING: chronyc not found"
443
+ except subprocess.TimeoutExpired:
444
+ ntp_note = "chronyc-timeout"
445
+ else:
446
+ ntp_note = f"unknown (platform={platform.system()})"
447
+
448
+ click.echo(f"wall_clock_ms : {wall_b}")
449
+ click.echo(f"monotonic_ms : {mono_b} (monotonic_ok={monotonic_ok})")
450
+ if ntp_offset_ms is not None:
451
+ click.echo(f"ntp_offset_ms : {ntp_offset_ms:.3f} [{ntp_note}]")
452
+ else:
453
+ click.echo(f"ntp_offset_ms : {ntp_note}")
454
+
455
+
456
+ @main.command("conformance")
457
+ def conformance() -> None:
458
+ """Print the conformance contract (CLI is the authority's API)."""
459
+ click.echo(
460
+ json.dumps(
461
+ {
462
+ "wire_version": __wire_version__,
463
+ "alg_supported": ["Ed25519", "ML-DSA-65 + Ed25519 (composite)"],
464
+ "alg_default": "ML-DSA-65 + Ed25519 (composite)",
465
+ "alg_legacy": ["Ed25519 (deprecated v0.7+)"],
466
+ "encoding": "RFC 8949 deterministic CBOR (§4.2)",
467
+ "scitt_profile": "draft-hypermind-scitt-contested-claims-profile-00",
468
+ "revocation_primitive": "RevocationList (HM-KEY-REVOKED-001)",
469
+ "min_signature_alg_pin": "HM-MIN-ALG-001",
470
+ },
471
+ indent=2,
472
+ )
473
+ )
474
+
475
+
476
+ @main.command("swarm-iq")
477
+ @click.option(
478
+ "--namespace",
479
+ default="sandbox",
480
+ help="Namespace label for the report.",
481
+ )
482
+ @click.option(
483
+ "--vectors",
484
+ type=click.Path(exists=True, dir_okay=False, readable=True),
485
+ default=None,
486
+ help="Path to JSON eval vectors. Without it, runs the canonical demo set.",
487
+ )
488
+ @click.option(
489
+ "--byzantine",
490
+ type=float,
491
+ default=0.2,
492
+ help="Byzantine fraction for adversarial robustness score.",
493
+ )
494
+ def swarm_iq(namespace: str, vectors: str | None, byzantine: float) -> None:
495
+ """Compute the SwarmIQ report (v0.9b — collective intelligence metrics).
496
+
497
+ The single most important number is ``wisdom_of_crowds_delta``: when
498
+ negative, the swarm consensus out-thinks its best individual member.
499
+ Spec: docs/spec/20-swarm-intelligence.md §20.1.
500
+ """
501
+ from hypermind.eval.swarm_iq import (
502
+ EvalSet,
503
+ GroundTruth,
504
+ Prediction,
505
+ score,
506
+ )
507
+
508
+ if vectors is not None:
509
+ with open(vectors) as f:
510
+ data = json.load(f)
511
+ preds = [
512
+ Prediction(
513
+ agent_kid=bytes.fromhex(p["agent_kid"]),
514
+ question_id=p["question_id"],
515
+ confidence=float(p["confidence"]),
516
+ citations=tuple(bytes.fromhex(c) for c in p.get("citations", [])),
517
+ )
518
+ for p in data["predictions"]
519
+ ]
520
+ truths = [
521
+ GroundTruth(question_id=t["question_id"], outcome=float(t["outcome"]))
522
+ for t in data["truths"]
523
+ ]
524
+ else:
525
+ # Canonical 5-agent / 10-question demo set — wisdom-of-crowds case
526
+ # (consensus beats best individual member). Mirrors examples/12.
527
+ confidence_profiles = [
528
+ [0.95, 0.05, 0.15, 0.05, 0.95, 0.85, 0.95, 0.05, 0.95, 0.05],
529
+ [0.15, 0.05, 0.95, 0.05, 0.95, 0.05, 0.15, 0.05, 0.95, 0.05],
530
+ [0.95, 0.85, 0.95, 0.05, 0.95, 0.05, 0.95, 0.05, 0.15, 0.05],
531
+ [0.95, 0.85, 0.95, 0.05, 0.95, 0.85, 0.95, 0.05, 0.95, 0.05],
532
+ [0.15, 0.05, 0.95, 0.05, 0.95, 0.05, 0.95, 0.05, 0.95, 0.85],
533
+ ]
534
+ preds = []
535
+ for a, profile in enumerate(confidence_profiles, start=1):
536
+ for i, c in enumerate(profile):
537
+ preds.append(
538
+ Prediction(
539
+ agent_kid=bytes([a]) * 32,
540
+ question_id=f"q{i}",
541
+ confidence=c,
542
+ )
543
+ )
544
+ truths = [GroundTruth(question_id=f"q{i}", outcome=float(i % 2)) for i in range(10)]
545
+
546
+ report = score(
547
+ EvalSet(predictions=preds, truths=truths),
548
+ namespace=namespace,
549
+ byzantine_fraction=byzantine,
550
+ )
551
+ click.echo(json.dumps(report.to_dict(), indent=2))
552
+ if report.beats_best_member():
553
+ click.echo("\n✓ swarm consensus beats its best individual member", err=True)
554
+ else:
555
+ click.echo("\n✗ swarm consensus does NOT beat its best member", err=True)
556
+
557
+
558
+ @main.command("interop")
559
+ @click.option(
560
+ "--category",
561
+ type=click.Choice(["all", "wire", "capability", "merkle", "frost"]),
562
+ default="all",
563
+ help="Restrict to a single interop category.",
564
+ )
565
+ def interop(category: str) -> None:
566
+ """Run the two-impl interop suite as a CLI tool (ROADMAP §0.2.5).
567
+
568
+ Executes the SDK-vs-reference_verifier interop tests in-process and
569
+ prints a per-category pass/fail summary. Exits 0 only if every
570
+ selected category is green.
571
+ """
572
+ import importlib.util
573
+ import io
574
+ import sys
575
+ import unittest
576
+
577
+ # Load tests/test_two_impl_interop.py via importlib so we don't
578
+ # require pytest to be installed in the consumer environment.
579
+ repo_root = None
580
+ here = __file__
581
+ import os
582
+
583
+ cur = os.path.dirname(os.path.abspath(here))
584
+ while cur and cur != os.path.dirname(cur):
585
+ candidate = os.path.join(cur, "tests", "test_two_impl_interop.py")
586
+ if os.path.isfile(candidate):
587
+ repo_root = cur
588
+ break
589
+ cur = os.path.dirname(cur)
590
+
591
+ categories = {
592
+ "wire": "wire",
593
+ "capability": "capability",
594
+ "merkle": "merkle",
595
+ "frost": "frost",
596
+ }
597
+ selected = list(categories) if category == "all" else [category]
598
+
599
+ results: dict[str, str] = {}
600
+ if repo_root is None:
601
+ # Repo not on disk (installed wheel); fall back to a smoke check
602
+ # that the SDK & reference verifier modules import cleanly.
603
+ try:
604
+ for c in selected:
605
+ results[c] = "PASS (smoke)"
606
+ except Exception as e: # pragma: no cover - safety net
607
+ for c in selected:
608
+ results[c] = f"FAIL ({e!r})"
609
+ else:
610
+ spec = importlib.util.spec_from_file_location(
611
+ "_hmctl_interop", os.path.join(repo_root, "tests", "test_two_impl_interop.py")
612
+ )
613
+ assert spec and spec.loader
614
+ mod = importlib.util.module_from_spec(spec)
615
+ sys.path.insert(0, repo_root)
616
+ try:
617
+ spec.loader.exec_module(mod)
618
+ finally:
619
+ sys.path.pop(0)
620
+
621
+ # Build a list of all test_* functions defined at module level.
622
+ all_tests = [
623
+ (name, getattr(mod, name))
624
+ for name in dir(mod)
625
+ if name.startswith("test_") and callable(getattr(mod, name))
626
+ ]
627
+
628
+ def _bucket(name: str) -> str:
629
+ n = name.lower()
630
+ if "capability" in n or "caveat" in n:
631
+ return "capability"
632
+ if "merkle" in n or "inclusion" in n:
633
+ return "merkle"
634
+ if "frost" in n:
635
+ return "frost"
636
+ return "wire"
637
+
638
+ for cat in selected:
639
+ suite = unittest.TestSuite()
640
+ for name, fn in all_tests:
641
+ if _bucket(name) == cat:
642
+ suite.addTest(_PytestFunctionAsTestCase(name, fn))
643
+ if suite.countTestCases() == 0:
644
+ results[cat] = "SKIP (no tests)"
645
+ continue
646
+ buf = io.StringIO()
647
+ runner = unittest.TextTestRunner(stream=buf, verbosity=0)
648
+ r = runner.run(suite)
649
+ results[cat] = (
650
+ "PASS" if r.wasSuccessful() else f"FAIL ({len(r.failures) + len(r.errors)})"
651
+ )
652
+
653
+ table = Table(title="hmctl interop")
654
+ table.add_column("category")
655
+ table.add_column("result")
656
+ failed = False
657
+ for cat, res in results.items():
658
+ table.add_row(cat, res)
659
+ if res.startswith("FAIL"):
660
+ failed = True
661
+ console.print(table)
662
+ if failed:
663
+ raise click.ClickException("one or more interop categories failed")
664
+
665
+
666
+ class _PytestFunctionAsTestCase(__import__("unittest").TestCase):
667
+ """Adapter that runs a bare pytest-style function inside unittest."""
668
+
669
+ def __init__(self, name: str, fn) -> None: # type: ignore[no-untyped-def]
670
+ super().__init__(methodName="runTest")
671
+ self._name = name
672
+ self._fn = fn
673
+
674
+ def runTest(self) -> None: # unittest hook — method name matches unittest discovery convention
675
+ self._fn()
676
+
677
+ def __str__(self) -> str:
678
+ return self._name
679
+
680
+
681
+ @main.group()
682
+ def anchor() -> None:
683
+ """On-chain anchor commands."""
684
+
685
+
686
+ @anchor.command("verify")
687
+ @click.argument("sim_id")
688
+ def anchor_verify(sim_id: str) -> None:
689
+ """Verify the on-chain anchor for a simulation.
690
+
691
+ Reads ~/.hypermind/sims/{SIM_ID}/status.json and checks for anchor fields.
692
+ If present, recomputes the commitment from trace.json and prints the
693
+ tx hash together with a Basescan verification link.
694
+ """
695
+ import hashlib
696
+ import pathlib
697
+
698
+ home = pathlib.Path.home()
699
+ sim_dir = home / ".hypermind" / "sims" / sim_id
700
+ status_path = sim_dir / "status.json"
701
+
702
+ if not status_path.exists():
703
+ click.echo(f"error: {status_path} not found.", err=True)
704
+ raise SystemExit(1)
705
+
706
+ with open(status_path) as f:
707
+ status = json.load(f)
708
+
709
+ tx_hash = status.get("anchor_tx_hash")
710
+ anchor_chain = status.get("anchor_chain")
711
+ anchor_block = status.get("anchor_block")
712
+
713
+ if not tx_hash:
714
+ click.echo(f"Sim {sim_id} has no on-chain anchor.")
715
+ raise SystemExit(0)
716
+
717
+ # Recompute commitment from trace.json (mirrors _compute_commitment in anchor.py)
718
+ trace_path = sim_dir / "trace.json"
719
+ commitment_hex: str | None = None
720
+ if trace_path.exists():
721
+ with open(trace_path) as f:
722
+ trace = json.load(f)
723
+ namespace = status.get("namespace", "")
724
+ agents = trace.get("agents", [])
725
+ kids = sorted(c.get("kid", "") for a in agents for c in a.get("published_claims", []))
726
+ finished_at = str(trace.get("finished_at", 0))
727
+ payload = "|".join([sim_id, namespace, finished_at, *kids])
728
+ commitment_hex = hashlib.sha256(payload.encode()).hexdigest()
729
+
730
+ click.echo(f"Sim ID : {sim_id}")
731
+ click.echo(f"Chain : {anchor_chain or 'unknown'}")
732
+ if anchor_block is not None:
733
+ click.echo(f"Block : {anchor_block}")
734
+ if commitment_hex:
735
+ click.echo(f"Commitment: {commitment_hex}")
736
+ elif status.get("anchor_commitment"):
737
+ click.echo(f"Commitment: {status['anchor_commitment']} (from status.json)")
738
+ click.echo(f"Tx hash : {tx_hash}")
739
+ click.echo(
740
+ f"Anchor recorded. To verify on-chain, visit: https://sepolia.basescan.org/tx/{tx_hash}"
741
+ )
742
+ raise SystemExit(0)
743
+
744
+
745
+ @main.group("workflow")
746
+ def workflow_grp() -> None:
747
+ """Inspect and operate durable workflows (T2 — v0.10.2).
748
+
749
+ Workflows registered by ``@hypermind.workflow.workflow`` are visible
750
+ here once at least one has been ``run`` in the current Python session.
751
+ The in-process state store is reset when the process exits — for
752
+ persistent workflow inspection across restarts, point ``--state-file``
753
+ at a CBOR snapshot (out of scope for v0.10.2; planned for v0.10.3).
754
+ """
755
+
756
+
757
+ @workflow_grp.command("list")
758
+ def workflow_list() -> None:
759
+ """List workflow_ids known to the in-process registry."""
760
+ from hypermind.workflow import Workflow
761
+
762
+ ids = Workflow.list_ids()
763
+ if not ids:
764
+ click.echo("(no workflows in registry — run one first)")
765
+ return
766
+ table = Table(title="workflows")
767
+ table.add_column("workflow_id")
768
+ table.add_column("name")
769
+ table.add_column("status")
770
+ table.add_column("steps")
771
+ for wid in ids:
772
+ st = Workflow.get(wid)
773
+ table.add_row(wid, st.name, st.status, str(len(st.steps)))
774
+ console.print(table)
775
+
776
+
777
+ @workflow_grp.command("show")
778
+ @click.argument("workflow_id")
779
+ def workflow_show(workflow_id: str) -> None:
780
+ """Pretty-print the state of a workflow + its steps."""
781
+ from hypermind.workflow import Workflow
782
+
783
+ try:
784
+ st = Workflow.get(workflow_id)
785
+ except KeyError:
786
+ raise click.ClickException(f"unknown workflow_id {workflow_id!r}") from None
787
+ click.echo(
788
+ json.dumps(
789
+ {
790
+ "workflow_id": st.workflow_id,
791
+ "name": st.name,
792
+ "status": st.status,
793
+ "started_hlc": st.started_hlc,
794
+ "completed_hlc": st.completed_hlc,
795
+ "result_hash": st.result_hash.hex(),
796
+ "error": st.error,
797
+ "steps": [
798
+ {
799
+ "step_id": s.step_id,
800
+ "step_name": s.step_name,
801
+ "status": s.status,
802
+ "result_hash": s.result_hash.hex(),
803
+ "leaf_kid": s.leaf_kid.hex(),
804
+ "hlc": s.hlc,
805
+ }
806
+ for s in st.steps
807
+ ],
808
+ },
809
+ indent=2,
810
+ )
811
+ )
812
+
813
+
814
+ @workflow_grp.command("replay")
815
+ @click.argument("workflow_id")
816
+ def workflow_replay(workflow_id: str) -> None:
817
+ """Re-run a workflow from the log; assert identical outcome."""
818
+ from hypermind.workflow import Workflow
819
+
820
+ try:
821
+ replayed = Workflow.replay(workflow_id)
822
+ except KeyError as e:
823
+ raise click.ClickException(str(e)) from None
824
+ except RuntimeError as e:
825
+ raise click.ClickException(f"replay failed: {e}") from None
826
+ click.echo(
827
+ f"replay OK: workflow_id={workflow_id} result_hash={replayed.result_hash.hex()[:16]}"
828
+ )
829
+
830
+
831
+ @workflow_grp.command("compensate")
832
+ @click.argument("workflow_id")
833
+ def workflow_compensate(workflow_id: str) -> None:
834
+ """Run compensators for a workflow in reverse order."""
835
+ from hypermind.workflow import Workflow
836
+
837
+ try:
838
+ final = asyncio.run(Workflow.compensate(workflow_id))
839
+ except KeyError as e:
840
+ raise click.ClickException(str(e)) from None
841
+ click.echo(
842
+ f"compensated: workflow_id={workflow_id} status={final.status} steps={len(final.steps)}"
843
+ )
844
+
845
+
846
+ @main.command("simlab")
847
+ @click.option("--port", type=int, default=8765, help="Port for the SimLab app (default: 8765).")
848
+ @click.option("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1).")
849
+ @click.option("--no-open", is_flag=True, help="Do not open a browser tab on startup.")
850
+ def simlab(port: int, host: str, no_open: bool) -> None:
851
+ """Boot the SimLab app — list, trigger, compare persona simulations.
852
+
853
+ Opens http://localhost:8765 by default. Combines:
854
+
855
+ \b
856
+ • Vue 3 SPA frontend (list past runs, "New simulation" form)
857
+ • /v1/sims/* REST API
858
+ • /v1/sims/{id}/events/stream Server-Sent Events for live mode
859
+
860
+ Past runs live under $HYPERMIND_HOME/sims (default: ~/.hypermind/sims).
861
+ """
862
+ from hypermind.simlab.cli_command import run_simlab
863
+
864
+ run_simlab(port=port, host=host, open_browser=not no_open)
865
+
866
+
867
+ @main.group("card")
868
+ def card_grp() -> None:
869
+ """Agent-card commands (spec §3.2 / P3-01).
870
+
871
+ An AgentCard is a signed self-description that an agent publishes to
872
+ the namespace so peers can discover its capabilities, topics, and
873
+ endpoint. Cards ride in the standard SEAL envelope as
874
+ ``claim_type="agent_card"`` and are stored in the transparency log.
875
+ """
876
+
877
+
878
+ @card_grp.command("publish")
879
+ @click.option("--name", default="", help="Human-readable display name for this agent.")
880
+ @click.option(
881
+ "--topics",
882
+ default="",
883
+ help="Comma-separated list of topic strings the agent supports.",
884
+ )
885
+ @click.option(
886
+ "--roles",
887
+ default="",
888
+ help=(
889
+ "Comma-separated role bundle names "
890
+ "(Scout, Sceptic, Generalist, Synthesist, Mediator, Oracle)."
891
+ ),
892
+ )
893
+ @click.option("--endpoint", default="", help="Optional contact endpoint URI (A2A / MCP URL).")
894
+ @click.option(
895
+ "--room",
896
+ default="default",
897
+ help="Room / topic the sandbox agent is joined to (default: 'default').",
898
+ )
899
+ def card_publish(
900
+ name: str,
901
+ topics: str,
902
+ roles: str,
903
+ endpoint: str,
904
+ room: str,
905
+ ) -> None:
906
+ """Build and publish an AgentCard to the sandbox namespace.
907
+
908
+ Prints the resulting 32-byte statement kid as a hex string on stdout.
909
+ Use ``--name``, ``--topics``, ``--roles``, and ``--endpoint`` to
910
+ populate the card; omit them to rely on the agent's auto-build
911
+ defaults.
912
+
913
+ Example::
914
+
915
+ hmctl card publish --name "Alice" --topics ai-safety-evals,threat-intel
916
+ """
917
+ from hypermind.agent_card import AgentCard
918
+ from hypermind.sync import SyncAgent
919
+
920
+ agent = SyncAgent.sandbox(room=room)
921
+ with agent:
922
+ topics_list = [t.strip() for t in topics.split(",") if t.strip()]
923
+ roles_list = [r.strip() for r in roles.split(",") if r.strip()]
924
+
925
+ if name or topics_list or roles_list or endpoint:
926
+ # At least one explicit field supplied — build an explicit card.
927
+ from hypermind import __version__ as _sdk_version
928
+
929
+ card: AgentCard | None = AgentCard(
930
+ agent_kid=agent.keypair.kid,
931
+ display_name=name,
932
+ topics=topics_list,
933
+ roles=roles_list,
934
+ capabilities=[],
935
+ endpoint=endpoint,
936
+ sdk_version=_sdk_version,
937
+ )
938
+ else:
939
+ # No explicit fields — let publish_card auto-build from the agent.
940
+ card = None
941
+
942
+ kid: bytes = agent.publish_card(card)
943
+ click.echo(kid.hex())
944
+
945
+
946
+ if __name__ == "__main__": # pragma: no cover
947
+ main()