hypermind 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hmctl/__init__.py +7 -0
- hmctl/cli.py +947 -0
- hypermind/__init__.py +208 -0
- hypermind/_deprecations.py +108 -0
- hypermind/agent.py +2896 -0
- hypermind/agent_card.py +127 -0
- hypermind/api/__init__.py +32 -0
- hypermind/api/app.py +686 -0
- hypermind/capability.py +923 -0
- hypermind/consult.py +493 -0
- hypermind/consult_bias.py +112 -0
- hypermind/contract_net.py +219 -0
- hypermind/crypto/__init__.py +21 -0
- hypermind/crypto/cose_encrypt.py +126 -0
- hypermind/crypto/dkg.py +246 -0
- hypermind/crypto/domain.py +59 -0
- hypermind/crypto/frost.py +771 -0
- hypermind/crypto/hashing.py +38 -0
- hypermind/crypto/hlc.py +163 -0
- hypermind/crypto/hpke.py +277 -0
- hypermind/crypto/kms.py +196 -0
- hypermind/crypto/kms_backends/__init__.py +56 -0
- hypermind/crypto/kms_backends/aws.py +128 -0
- hypermind/crypto/kms_backends/azure.py +114 -0
- hypermind/crypto/kms_backends/gcp.py +97 -0
- hypermind/crypto/kms_backends/vault.py +120 -0
- hypermind/crypto/namespace_root.py +204 -0
- hypermind/crypto/pq.py +152 -0
- hypermind/crypto/room_epoch.py +97 -0
- hypermind/crypto/signing.py +245 -0
- hypermind/deliberation.py +355 -0
- hypermind/did_web.py +256 -0
- hypermind/dispute.py +858 -0
- hypermind/errors.py +218 -0
- hypermind/eval/__init__.py +49 -0
- hypermind/eval/calibration.py +307 -0
- hypermind/eval/comparison.py +251 -0
- hypermind/eval/replay.py +202 -0
- hypermind/eval/swarm_iq.py +718 -0
- hypermind/knowledge/__init__.py +17 -0
- hypermind/knowledge/citation_graph.py +141 -0
- hypermind/knowledge/correlated_agreement.py +167 -0
- hypermind/knowledge/embedding_registry.py +115 -0
- hypermind/knowledge/kernel_store.py +197 -0
- hypermind/knowledge/rekor.py +173 -0
- hypermind/knowledge/reputation.py +364 -0
- hypermind/knowledge/swarm_memory.py +523 -0
- hypermind/knowledge/transparency.py +409 -0
- hypermind/mcp/__init__.py +60 -0
- hypermind/mcp/bridge.py +151 -0
- hypermind/mcp/client.py +142 -0
- hypermind/mcp/provenance.py +214 -0
- hypermind/mcp/relata_tools.py +152 -0
- hypermind/mcp/server.py +248 -0
- hypermind/mcp/transport.py +206 -0
- hypermind/mind/__init__.py +150 -0
- hypermind/mind/active.py +234 -0
- hypermind/mind/belief.py +369 -0
- hypermind/mind/deliberator.py +625 -0
- hypermind/mind/diversity.py +144 -0
- hypermind/mind/evolve.py +184 -0
- hypermind/mind/federation.py +154 -0
- hypermind/mind/goals.py +117 -0
- hypermind/mind/integration.py +190 -0
- hypermind/mind/mediator.py +74 -0
- hypermind/mind/memory_store.py +129 -0
- hypermind/mind/policy.py +307 -0
- hypermind/mind/records.py +260 -0
- hypermind/mind/roles.py +190 -0
- hypermind/mind/sybil_guard.py +41 -0
- hypermind/mind/world_model.py +166 -0
- hypermind/namespace.py +515 -0
- hypermind/namespace_policy.py +254 -0
- hypermind/observability/__init__.py +25 -0
- hypermind/observability/asgi.py +214 -0
- hypermind/observability/cost.py +277 -0
- hypermind/observability/otel_export.py +200 -0
- hypermind/observability/recorder.py +344 -0
- hypermind/observability/swarm_trace.py +438 -0
- hypermind/pin_policy.py +116 -0
- hypermind/py.typed +0 -0
- hypermind/responders/__init__.py +87 -0
- hypermind/responders/anthropic.py +159 -0
- hypermind/responders/base.py +162 -0
- hypermind/responders/openai.py +199 -0
- hypermind/responders/router.py +254 -0
- hypermind/responders/structured.py +287 -0
- hypermind/responders/tools.py +444 -0
- hypermind/revocation.py +270 -0
- hypermind/rule_ids.py +135 -0
- hypermind/schemas/encrypted_statement.cddl +28 -0
- hypermind/schemas/namespace_accept.cddl +20 -0
- hypermind/schemas/namespace_create.cddl +25 -0
- hypermind/schemas/namespace_founding_attest.cddl +30 -0
- hypermind/schemas/namespace_invite.cddl +22 -0
- hypermind/schemas/wire-v0.1.cddl +73 -0
- hypermind/simlab/__init__.py +15 -0
- hypermind/simlab/_persona_registry.py +93 -0
- hypermind/simlab/_scenario_impl.py +2778 -0
- hypermind/simlab/app.py +2538 -0
- hypermind/simlab/backends/__init__.py +27 -0
- hypermind/simlab/backends/anchor.py +88 -0
- hypermind/simlab/backends/local.py +32 -0
- hypermind/simlab/backends/server.py +79 -0
- hypermind/simlab/cli_command.py +108 -0
- hypermind/simlab/config.py +106 -0
- hypermind/simlab/deployment.py +26 -0
- hypermind/simlab/knowledge.py +716 -0
- hypermind/simlab/learning.py +295 -0
- hypermind/simlab/modes/__init__.py +115 -0
- hypermind/simlab/modes/_eval_real_llm.py +373 -0
- hypermind/simlab/modes/aar.py +611 -0
- hypermind/simlab/modes/backtest.py +610 -0
- hypermind/simlab/modes/binary_forecast.py +69 -0
- hypermind/simlab/modes/conformance.py +1869 -0
- hypermind/simlab/modes/evaluation.py +2271 -0
- hypermind/simlab/modes/governance.py +648 -0
- hypermind/simlab/modes/redteam.py +534 -0
- hypermind/simlab/modes/tabletop.py +797 -0
- hypermind/simlab/modes/tournament.py +448 -0
- hypermind/simlab/modes/whatif.py +523 -0
- hypermind/simlab/namespace.py +125 -0
- hypermind/simlab/personas.py +477 -0
- hypermind/simlab/prompt_generator.py +172 -0
- hypermind/simlab/question_sets/geopolitics_q20.json +122 -0
- hypermind/simlab/question_sets/governance_q5.json +67 -0
- hypermind/simlab/question_sets/msft_outlook_q10.json +62 -0
- hypermind/simlab/question_sets/whatif_q3.json +38 -0
- hypermind/simlab/registry.py +325 -0
- hypermind/simlab/runner.py +239 -0
- hypermind/simlab/scenario.py +147 -0
- hypermind/simlab/swarms.py +180 -0
- hypermind/simlab/tool_handlers/__init__.py +4 -0
- hypermind/simlab/tool_handlers/alpha_vantage.py +39 -0
- hypermind/simlab/tool_handlers/brave.py +46 -0
- hypermind/simlab/tool_handlers/newsapi.py +50 -0
- hypermind/simlab/tool_handlers/openweather.py +46 -0
- hypermind/simlab/tool_handlers/pinecone.py +52 -0
- hypermind/simlab/tool_handlers/qdrant.py +57 -0
- hypermind/simlab/tool_handlers/query_knowledge_base.py +21 -0
- hypermind/simlab/tool_handlers/relata_recall.py +61 -0
- hypermind/simlab/tool_handlers/retrieve_document.py +21 -0
- hypermind/simlab/tool_handlers/serper.py +46 -0
- hypermind/simlab/tool_handlers/tavily.py +53 -0
- hypermind/simlab/tool_handlers/weaviate.py +65 -0
- hypermind/simlab/tool_handlers/wikipedia.py +64 -0
- hypermind/simlab/tool_handlers/wolfram.py +48 -0
- hypermind/simlab/tool_handlers/yahoo_finance.py +49 -0
- hypermind/simlab/tool_registry.py +568 -0
- hypermind/simlab/topic_adapter.py +338 -0
- hypermind/simlab/topic_questions.py +259 -0
- hypermind/storage/__init__.py +69 -0
- hypermind/storage/backend.py +71 -0
- hypermind/storage/relata.py +245 -0
- hypermind/storage/sqlite.py +258 -0
- hypermind/sync.py +191 -0
- hypermind/tal.py +175 -0
- hypermind/tasks.py +334 -0
- hypermind/testing.py +16 -0
- hypermind/tools/__init__.py +32 -0
- hypermind/tools/registry.py +61 -0
- hypermind/transport/__init__.py +22 -0
- hypermind/transport/anti_entropy.py +708 -0
- hypermind/transport/bus.py +206 -0
- hypermind/transport/knows_delta.py +204 -0
- hypermind/transport/libp2p.py +298 -0
- hypermind/transport/placement.py +58 -0
- hypermind/transport/tcp.py +797 -0
- hypermind/transport/tls_profile.py +417 -0
- hypermind/types.py +59 -0
- hypermind/uncertainty.py +222 -0
- hypermind/wire.py +897 -0
- hypermind/workflow/__init__.py +72 -0
- hypermind/workflow/engine.py +655 -0
- hypermind/workflow/plan.py +182 -0
- hypermind/workflow/repair.py +203 -0
- hypermind-0.11.0.dist-info/METADATA +524 -0
- hypermind-0.11.0.dist-info/RECORD +181 -0
- hypermind-0.11.0.dist-info/WHEEL +4 -0
- hypermind-0.11.0.dist-info/entry_points.txt +2 -0
- hypermind-0.11.0.dist-info/licenses/LICENSE +201 -0
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()
|