hyperagents 1.2.3rc5.post3__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 (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,341 @@
1
+ """Tool-use qualification records — signed by Hyper (spec §11 + cross-cutting
2
+ rule 3; CEO rulings 2026-10-03).
3
+
4
+ A provider earns ``tool_use`` only by passing the qualification suite — the
5
+ same rule for HyperSpeed V2 and for a customer's own provider. models.yaml
6
+ may *declare* tool_use; it is *granted* only when a **Hyper-signed** record
7
+ for that exact provider + model exists and says QUALIFIED. Otherwise the
8
+ model is chat-only. There is no grace period: a model without a valid signed
9
+ record has no tools.
10
+
11
+ Record file (one per provider+model, e.g. ``anthropic--claude-sonnet-5-5.json``)::
12
+
13
+ {"record": {"provider": "anthropic", "model": "claude-sonnet-5-5",
14
+ "verdict": "QUALIFIED", # or NOT_QUALIFIED
15
+ "suite_version": "...", "benchmark_run_id": "...",
16
+ "harness": "<harness sha/version>",
17
+ "hyperclaw_version": "1.2.3rc2",
18
+ "issued_at": "2026-10-03T20:00:00Z",
19
+ "expires_at": "2027-10-03T00:00:00Z", # optional
20
+ "key_id": "<issuer key id>"},
21
+ "signature": "<base64 Ed25519 signature over canonical(record)>"}
22
+
23
+ ``canonical(record)`` is ``json.dumps(record, sort_keys=True,
24
+ separators=(",", ":"), ensure_ascii=False).encode("utf-8")``. The signature
25
+ is detached from the payload: it covers the record object only.
26
+
27
+ Verification uses Hyper's public key(s) shipped in the package
28
+ (``hyperclaw/qualification_keys/<key_id>.pub``, base64 raw Ed25519 key),
29
+ selected by the record's ``key_id``. Unsigned, bad-signature, unknown-key,
30
+ malformed or expired records are IGNORED (logged once) and the model stays
31
+ chat-only.
32
+
33
+ Locations: the package (``hyperclaw/qualification_records/``, which Hyper
34
+ ships when a model passes) and the install (``$HYPERCLAW_ROOT/qualification/``,
35
+ default ``~/.hyperclaw/qualification/``). Both must verify. A Hyper-deployed
36
+ install honours packaged records only. The provider and model *inside* the
37
+ signed record must match; the filename alone proves nothing.
38
+
39
+ Signing (maintainers only): ``hyperclaw-sign-qualification`` /
40
+ ``python -m hyperclaw.qualification sign`` reads the private key from the
41
+ macOS keychain (generic password, service ``hc-qual-signing-key``, account
42
+ ``hyper``) and writes a signed record from a benchmark ``results-v2.json``.
43
+ """
44
+ from __future__ import annotations
45
+
46
+ import argparse
47
+ import base64
48
+ import datetime as _dt
49
+ import hashlib
50
+ import json
51
+ import logging
52
+ import re
53
+ import subprocess
54
+ import sys
55
+ from pathlib import Path
56
+ from typing import Optional
57
+
58
+ log = logging.getLogger("hyperclaw.qualification")
59
+
60
+ QUALIFIED = "QUALIFIED"
61
+ NOT_QUALIFIED = "NOT_QUALIFIED"
62
+ VERDICTS = (QUALIFIED, NOT_QUALIFIED)
63
+ RECORD_FIELDS_REQUIRED = ("provider", "model", "verdict", "suite_version",
64
+ "benchmark_run_id", "harness", "hyperclaw_version",
65
+ "issued_at", "key_id")
66
+
67
+ _PKG = Path(__file__).resolve().parent
68
+ PACKAGED_DIR = _PKG / "qualification_records"
69
+ KEYS_DIR = _PKG / "qualification_keys"
70
+
71
+ KEYCHAIN_SERVICE = "hc-qual-signing-key"
72
+ KEYCHAIN_ACCOUNT = "hyper"
73
+
74
+ # Extra trusted public keys {key_id: raw 32-byte key}. Empty in production;
75
+ # the unit tests add a throwaway test key here (never the production key).
76
+ EXTRA_TRUSTED_KEYS: dict = {}
77
+
78
+ _logged: set = set()
79
+
80
+
81
+ # ───────────────────────── helpers ─────────────────────────
82
+
83
+ def _slug(model: str) -> str:
84
+ return re.sub(r"[^A-Za-z0-9._-]+", "_", model or "").strip("_") or "default"
85
+
86
+
87
+ def record_filename(provider: str, model: str) -> str:
88
+ return f"{_slug(provider)}--{_slug(model)}.json"
89
+
90
+
91
+ def user_dir() -> Path:
92
+ from hyperclaw.userconfig import root
93
+ return root() / "qualification"
94
+
95
+
96
+ def _dirs() -> list:
97
+ from hyperclaw.deployment import is_hyper
98
+ return [PACKAGED_DIR] if is_hyper() else [PACKAGED_DIR, user_dir()]
99
+
100
+
101
+ def canonical(record: dict) -> bytes:
102
+ return json.dumps(record, sort_keys=True, separators=(",", ":"),
103
+ ensure_ascii=False).encode("utf-8")
104
+
105
+
106
+ def key_id_for(public_raw: bytes) -> str:
107
+ """Key id = 'hyper-' + first 16 hex chars of sha256(raw public key)."""
108
+ return "hyper-" + hashlib.sha256(public_raw).hexdigest()[:16]
109
+
110
+
111
+ def _now() -> _dt.datetime:
112
+ return _dt.datetime.now(_dt.timezone.utc)
113
+
114
+
115
+ def _parse_ts(s: str) -> _dt.datetime:
116
+ t = _dt.datetime.fromisoformat(str(s).replace("Z", "+00:00"))
117
+ return t if t.tzinfo else t.replace(tzinfo=_dt.timezone.utc)
118
+
119
+
120
+ def _log_once(path: Path, reason: str) -> None:
121
+ k = (str(path), reason)
122
+ if k not in _logged:
123
+ _logged.add(k)
124
+ log.warning("qualification record ignored (%s): %s — model stays chat-only",
125
+ reason, path)
126
+
127
+
128
+ def trusted_keys() -> dict:
129
+ """{key_id: raw public key bytes} from the package (+ EXTRA_TRUSTED_KEYS)."""
130
+ keys: dict = {}
131
+ try:
132
+ for p in sorted(KEYS_DIR.glob("*.pub")):
133
+ try:
134
+ raw = base64.b64decode(p.read_text(encoding="utf-8").strip(), validate=True)
135
+ except Exception:
136
+ continue
137
+ if len(raw) == 32 and key_id_for(raw) == p.stem:
138
+ keys[p.stem] = raw
139
+ except Exception:
140
+ pass
141
+ keys.update(EXTRA_TRUSTED_KEYS)
142
+ return keys
143
+
144
+
145
+ # ───────────────────────── verification ─────────────────────────
146
+
147
+ def verify_envelope(env, path: Path = Path("<memory>")) -> Optional[dict]:
148
+ """Return the verified record, or None (and log once why it was ignored)."""
149
+ from cryptography.exceptions import InvalidSignature
150
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
151
+
152
+ if not isinstance(env, dict) or not isinstance(env.get("record"), dict):
153
+ _log_once(path, "unsigned or malformed")
154
+ return None
155
+ rec, sig = env["record"], env.get("signature")
156
+ if not isinstance(sig, str) or not sig:
157
+ _log_once(path, "unsigned")
158
+ return None
159
+ if any(not rec.get(f) for f in RECORD_FIELDS_REQUIRED) or rec.get("verdict") not in VERDICTS:
160
+ _log_once(path, "missing fields")
161
+ return None
162
+ raw = trusted_keys().get(rec.get("key_id"))
163
+ if raw is None:
164
+ _log_once(path, "unknown key")
165
+ return None
166
+ try:
167
+ Ed25519PublicKey.from_public_bytes(raw).verify(base64.b64decode(sig, validate=True),
168
+ canonical(rec))
169
+ except (InvalidSignature, ValueError, TypeError):
170
+ _log_once(path, "bad signature")
171
+ return None
172
+ if rec.get("expires_at"):
173
+ try:
174
+ if _now() >= _parse_ts(rec["expires_at"]):
175
+ _log_once(path, "expired")
176
+ return None
177
+ except Exception:
178
+ _log_once(path, "bad expires_at")
179
+ return None
180
+ return rec
181
+
182
+
183
+ def find_record(provider: str, model: str) -> Optional[dict]:
184
+ """The first *verified* record for this exact provider + model, or None."""
185
+ if not provider or not model:
186
+ return None
187
+ name = record_filename(provider, model)
188
+ for d in _dirs():
189
+ p = d / name
190
+ try:
191
+ if not p.is_file():
192
+ continue
193
+ env = json.loads(p.read_text(encoding="utf-8"))
194
+ except Exception:
195
+ _log_once(p, "unreadable")
196
+ continue
197
+ rec = verify_envelope(env, p)
198
+ if rec and rec.get("provider") == provider and rec.get("model") == model:
199
+ return rec
200
+ if rec:
201
+ _log_once(p, "provider/model mismatch")
202
+ return None
203
+
204
+
205
+ def is_qualified(provider: str, model: str) -> bool:
206
+ rec = find_record(provider, model)
207
+ return bool(rec) and rec.get("verdict") == QUALIFIED
208
+
209
+
210
+ def status_label(provider: str, model: str) -> str:
211
+ """'tools (qualified)' or 'chat-only (not qualified for tools)'."""
212
+ return ("tools (qualified)" if is_qualified(provider, model)
213
+ else "chat-only (not qualified for tools)")
214
+
215
+
216
+ # ───────────────────────── signing ─────────────────────────
217
+
218
+ def _private_key(private_raw: bytes):
219
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
220
+ return Ed25519PrivateKey.from_private_bytes(private_raw)
221
+
222
+
223
+ def _public_raw(private_raw: bytes) -> bytes:
224
+ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
225
+ return _private_key(private_raw).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
226
+
227
+
228
+ def sign_record(record: dict, private_raw: bytes) -> dict:
229
+ """Return the signed envelope. ``key_id`` is set from the key."""
230
+ rec = dict(record)
231
+ rec["key_id"] = key_id_for(_public_raw(private_raw))
232
+ if rec.get("verdict") not in VERDICTS:
233
+ raise ValueError(f"verdict must be one of {VERDICTS}")
234
+ sig = _private_key(private_raw).sign(canonical(rec))
235
+ return {"record": rec, "signature": base64.b64encode(sig).decode("ascii")}
236
+
237
+
238
+ def make_record(provider: str, model: str, verdict: str, *, suite_version: str,
239
+ benchmark_run_id: str, harness: str, expires_at: Optional[str] = None,
240
+ hyperclaw_version: Optional[str] = None) -> dict:
241
+ if hyperclaw_version is None:
242
+ from hyperclaw import __version__ as hyperclaw_version
243
+ rec = {"provider": provider, "model": model, "verdict": verdict,
244
+ "suite_version": suite_version, "benchmark_run_id": benchmark_run_id,
245
+ "harness": harness, "hyperclaw_version": hyperclaw_version,
246
+ "issued_at": _now().replace(microsecond=0).isoformat().replace("+00:00", "Z")}
247
+ if expires_at:
248
+ rec["expires_at"] = expires_at
249
+ return rec
250
+
251
+
252
+ def write_envelope(env: dict, directory: Path) -> Path:
253
+ directory.mkdir(parents=True, exist_ok=True)
254
+ rec = env["record"]
255
+ p = directory / record_filename(rec["provider"], rec["model"])
256
+ p.write_text(json.dumps(env, indent=2, sort_keys=True) + "\n", encoding="utf-8")
257
+ return p
258
+
259
+
260
+ def verdict_from_results(results: dict) -> str:
261
+ """QUALIFIED only if the results file's qualification verdict says so."""
262
+ q = results.get("qualification") if isinstance(results, dict) else None
263
+ if (isinstance(q, dict) and q.get("qualified") is True
264
+ and str(q.get("line", "")).startswith(QUALIFIED) and not q.get("reasons")):
265
+ return QUALIFIED
266
+ return NOT_QUALIFIED
267
+
268
+
269
+ def keychain_private_key() -> bytes:
270
+ """Read the signing key from the macOS keychain. Never printed or logged."""
271
+ out = subprocess.run(["security", "find-generic-password", "-s", KEYCHAIN_SERVICE,
272
+ "-a", KEYCHAIN_ACCOUNT, "-w"],
273
+ capture_output=True, text=True, check=False)
274
+ if out.returncode != 0 or not out.stdout.strip():
275
+ raise SystemExit(f"signing key not found in keychain (service {KEYCHAIN_SERVICE!r}, "
276
+ f"account {KEYCHAIN_ACCOUNT!r})")
277
+ raw = base64.b64decode(out.stdout.strip())
278
+ if len(raw) != 32:
279
+ raise SystemExit("keychain item is not a raw Ed25519 private key")
280
+ return raw
281
+
282
+
283
+ def sign_main(argv=None, *, private_key_loader=keychain_private_key) -> int:
284
+ ap = argparse.ArgumentParser(
285
+ prog="hyperclaw-sign-qualification",
286
+ description="Write a Hyper-signed qualification record from a benchmark "
287
+ "results-v2.json (maintainers only; key from the macOS keychain).")
288
+ ap.add_argument("results", type=Path, help="benchmark results-v2.json")
289
+ ap.add_argument("--provider", required=True, help="HyperClaw provider id, e.g. anthropic")
290
+ ap.add_argument("--model", required=True, help="exact model id HyperClaw sends")
291
+ ap.add_argument("--suite-version", help="default: results.suite_version")
292
+ ap.add_argument("--harness", help="harness sha/version; default: results.harness_sha "
293
+ "or results.source_commit")
294
+ ap.add_argument("--expires-at", help="optional ISO-8601 expiry")
295
+ ap.add_argument("--out", type=Path, default=PACKAGED_DIR,
296
+ help=f"output directory (default: the package, {PACKAGED_DIR})")
297
+ a = ap.parse_args(argv)
298
+
299
+ results = json.loads(a.results.read_text(encoding="utf-8"))
300
+ model_info = results.get("model") if isinstance(results.get("model"), dict) else {}
301
+ if model_info.get("production") is False:
302
+ print("refused: results come from a non-production (mock) run", file=sys.stderr)
303
+ return 2
304
+ run_id = str(results.get("run_id") or "").strip()
305
+ suite = a.suite_version or str(results.get("suite_version") or "").strip()
306
+ harness = a.harness or str(results.get("harness_sha") or results.get("source_commit")
307
+ or "").strip()
308
+ missing = [n for n, v in (("run_id", run_id), ("--suite-version", suite),
309
+ ("--harness", harness)) if not v]
310
+ if missing:
311
+ print(f"refused: missing {', '.join(missing)}", file=sys.stderr)
312
+ return 2
313
+ verdict = verdict_from_results(results)
314
+ rec = make_record(a.provider, a.model, verdict, suite_version=suite,
315
+ benchmark_run_id=run_id, harness=harness, expires_at=a.expires_at)
316
+ env = sign_record(rec, private_key_loader())
317
+ if verify_envelope(env) is None:
318
+ print("refused: signing key is not one of the package's trusted keys", file=sys.stderr)
319
+ return 3
320
+ p = write_envelope(env, a.out)
321
+ print(f"{verdict}: {a.provider}/{a.model} -> {p} (key {env['record']['key_id']})")
322
+ return 0
323
+
324
+
325
+ def main(argv=None) -> int:
326
+ argv = list(sys.argv[1:] if argv is None else argv)
327
+ if argv[:1] == ["sign"]:
328
+ return sign_main(argv[1:])
329
+ if argv[:1] == ["verify"] and len(argv) == 2:
330
+ p = Path(argv[1])
331
+ rec = verify_envelope(json.loads(p.read_text(encoding="utf-8")), p)
332
+ print("VALID " + rec["verdict"] if rec else "IGNORED (see warning)")
333
+ return 0 if rec else 1
334
+ print("usage: python -m hyperclaw.qualification sign RESULTS --provider P --model M ...\n"
335
+ " python -m hyperclaw.qualification verify RECORD.json", file=sys.stderr)
336
+ return 2
337
+
338
+
339
+ if __name__ == "__main__":
340
+ logging.basicConfig()
341
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ HAMPPhfeVAwcXSgTBJDz4gLJ5x8KUP5weURs4WDR9ig=
@@ -0,0 +1,14 @@
1
+ {
2
+ "record": {
3
+ "benchmark_run_id": "20261013-205229-hc1.2.3rc4.post2-anthropic-claude-sonnet-5-5",
4
+ "harness": "anthropic-qual-2026-10-03b",
5
+ "hyperclaw_version": "1.2.3rc4.post2",
6
+ "issued_at": "2026-10-14T03:54:09Z",
7
+ "key_id": "hyper-d292fd127cef5a3e",
8
+ "model": "claude-sonnet-5-5",
9
+ "provider": "anthropic",
10
+ "suite_version": "hc-qual-13@235d29339fd8",
11
+ "verdict": "QUALIFIED"
12
+ },
13
+ "signature": "LoBWMZtwy1R1ieowq/OfPqHLqL/ZdvtkZW98iT509JIYrDeEFOpZ/nhOnbpZGdTzehu9tXgGBiG1pqGDnXSbAw=="
14
+ }
@@ -0,0 +1,35 @@
1
+ """Whether raw model reasoning may be shown to a person.
2
+
3
+ Vaughn P1 (2026-10-07): raw reasoning can leak internal instruction wording,
4
+ so until a reasoning filter lands it is OFF by default on every surface (the
5
+ Telegram 💭 preview, the dashboard reasoning stream) and FORCED OFF on a
6
+ Hyper-deployed install (HYPERCLAW_DEPLOYMENT=hyper). An owner on a standard
7
+ install may opt in explicitly:
8
+
9
+ HYPERCLAW_SHOW_REASONING=1 every surface
10
+ TELEGRAM_SHOW_THINKING=1 Telegram preview only (older setting)
11
+
12
+ Reasoning is still CLASSIFIED (hyperclaw.oai_stream) so it never becomes the
13
+ answer; this only decides whether it is displayed.
14
+ """
15
+ import os
16
+
17
+ _TRUE = ("1", "true", "yes", "on")
18
+
19
+
20
+ def _flag(name: str) -> bool:
21
+ return os.environ.get(name, "").strip().lower() in _TRUE
22
+
23
+
24
+ def show_reasoning(surface: str = "") -> bool:
25
+ from hyperclaw.deployment import is_hyper
26
+ try:
27
+ if is_hyper():
28
+ return False
29
+ except Exception:
30
+ return False # deployment unknown: fail closed
31
+ if _flag("HYPERCLAW_SHOW_REASONING"):
32
+ return True
33
+ if surface == "telegram" and _flag("TELEGRAM_SHOW_THINKING"):
34
+ return True
35
+ return False