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,323 @@
1
+ """Resolve terminal credentials without changing the environment or saved settings."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Mapping
6
+ from urllib.parse import urlsplit
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class TerminalConfigurationError(ValueError):
12
+ """An actionable configuration problem that must be caught before an API call."""
13
+
14
+
15
+ class TerminalProvider(BaseModel):
16
+ model_config = ConfigDict(frozen=True)
17
+
18
+ name: str
19
+ kind: str = "openai_compat" # "anthropic" (Messages API) or "openai_compat"
20
+ api_key: str = Field(repr=False)
21
+ model: str
22
+ base_url: str | None = None
23
+ label: str = "" # banner label of the matching registry provider
24
+
25
+ @property
26
+ def display_label(self) -> str:
27
+ if self.label:
28
+ return self.label
29
+ # Customer providers only: Hyper models always carry their own label.
30
+ try:
31
+ from hyperclaw.provider_setup import OWN_KINDS
32
+ name = OWN_KINDS.get(self.name, self.name)
33
+ except Exception: # noqa: BLE001 -- a label must never break startup
34
+ name = self.name
35
+ return f"your own provider ({name})"
36
+
37
+
38
+ def _is_hyper_endpoint(prefix: str, base_url: str, env: Mapping[str, str]) -> bool:
39
+ """True when the resolved OpenAI-compatible group is Hyper's own model:
40
+ the HYPERSPEED_* group, or any group pointing at the HyperSpeed endpoint."""
41
+ from hyperclaw import hyperspeed
42
+ if prefix == "HYPERSPEED":
43
+ return True
44
+ hyper_urls = {hyperspeed.DEFAULT_BASE_URL.rstrip("/"),
45
+ (env.get(hyperspeed.BASE_URL_ENV, "") or "").strip().rstrip("/")} - {""}
46
+ return base_url.strip().rstrip("/") in hyper_urls
47
+
48
+
49
+ def _hyper_label(base_url: str) -> str:
50
+ from hyperclaw import hyperspeed
51
+ if base_url.strip().rstrip("/") != hyperspeed.DEFAULT_BASE_URL.rstrip("/"):
52
+ return f"{hyperspeed.LABEL} (custom endpoint)"
53
+ return hyperspeed.LABEL
54
+
55
+
56
+ def _validated_url(base_url: str) -> str:
57
+ try:
58
+ parsed = urlsplit(base_url)
59
+ valid = (parsed.scheme in ("http", "https") and parsed.hostname
60
+ and not parsed.username and not parsed.password)
61
+ parsed.port # Validate malformed ports before constructing an SDK client.
62
+ except ValueError:
63
+ valid = False
64
+ if not valid:
65
+ raise TerminalConfigurationError(
66
+ "The configured model URL must be an HTTP(S) URL without embedded credentials."
67
+ )
68
+ return base_url.rstrip("/")
69
+
70
+
71
+ def resolve_terminal_provider(env: Mapping[str, str] | None = None) -> TerminalProvider:
72
+ env = os.environ if env is None else env
73
+
74
+ def value(name: str) -> str:
75
+ return env.get(name, "").strip()
76
+
77
+ # Keep each endpoint paired with its own credentials. An OpenAI key must
78
+ # never be combined with a different provider's URL as a field fallback.
79
+ openai_url = value("OPENAI_BASE_URL") or value("OPENAI_API_BASE")
80
+ hyperspeed_url = value("HYPERSPEED_BASE_URL")
81
+ name = value("LLM_PROVIDER")
82
+ if not name:
83
+ prefix = "OPENAI" if openai_url or not hyperspeed_url else "HYPERSPEED"
84
+ compatible_ready = (
85
+ (openai_url or hyperspeed_url) and value(f"{prefix}_API_KEY")
86
+ and (value(f"{prefix}_MODEL") or value("HYPERCLAW_MODEL"))
87
+ )
88
+ name = "openai_compat" if compatible_ready else "anthropic"
89
+
90
+ if name == "bedrock":
91
+ # Bedrock (a customer's own AWS provider, chat-only until qualified)
92
+ # is started by `hyperclaw start` itself, never through this resolver.
93
+ raise TerminalConfigurationError(
94
+ "Bedrock is not an endpoint this resolver can select. If an earlier "
95
+ "Hyperspeed outage selected Bedrock, restart with "
96
+ "LLM_PROVIDER=openai_compat hyperclaw start using your existing endpoint "
97
+ "and key. Saved settings have not been changed."
98
+ )
99
+
100
+ if name == "openai_compat":
101
+ prefix = "OPENAI" if openai_url or not hyperspeed_url else "HYPERSPEED"
102
+ base_url = openai_url if prefix == "OPENAI" else hyperspeed_url
103
+ api_key = value(f"{prefix}_API_KEY")
104
+ model = value(f"{prefix}_MODEL") or value("HYPERCLAW_MODEL")
105
+ if not base_url or not api_key or not model:
106
+ raise TerminalConfigurationError(
107
+ f"Configure {prefix}_BASE_URL, {prefix}_API_KEY and {prefix}_MODEL "
108
+ "for the same endpoint (OPENAI_API_BASE is also accepted). "
109
+ "Existing credentials have not been changed."
110
+ )
111
+ checked_url = _validated_url(base_url)
112
+ label = _hyper_label(checked_url) if _is_hyper_endpoint(prefix, checked_url, env) else ""
113
+ return TerminalProvider(name=name, kind="openai_compat", api_key=api_key, model=model,
114
+ base_url=checked_url, label=label)
115
+
116
+ if name not in ("anthropic", "anthropic_compat"):
117
+ raise TerminalConfigurationError(
118
+ "Unsupported LLM_PROVIDER. Select openai_compat, anthropic or anthropic_compat."
119
+ )
120
+ api_key = value("ANTHROPIC_API_KEY")
121
+ if not api_key:
122
+ raise TerminalConfigurationError("ANTHROPIC_API_KEY is not set for the selected provider.")
123
+ base_url = value("ANTHROPIC_BASE_URL")
124
+ if name == "anthropic_compat" and not base_url:
125
+ raise TerminalConfigurationError("ANTHROPIC_BASE_URL is required for anthropic_compat.")
126
+ return TerminalProvider(
127
+ name=name, kind="anthropic", api_key=api_key, model=value("HYPERCLAW_MODEL") or "claude-sonnet-4-6",
128
+ base_url=_validated_url(base_url) if base_url else None,
129
+ )
130
+
131
+
132
+ # ───────────────────── terminal route (provider + tool gating) ─────────────────────
133
+ #
134
+ # Two contracts meet here:
135
+ # * terminal recovery (MR !1): an explicit LLM_PROVIDER is always honored,
136
+ # credentials are never mixed across endpoint groups, endpoints are
137
+ # validated before a client exists, Bedrock is refused with recovery text,
138
+ # and nothing rewrites saved settings.
139
+ # * truth guard (release 1.2.1/1.2.2/1.2.3): the terminal gets tools ONLY
140
+ # from a provider that declares ``tool_use`` in models.yaml AND holds a
141
+ # Hyper-signed qualification record; anything else runs chat-only, and
142
+ # nothing configured means refuse to run (fail closed). A Hyper install
143
+ # uses Hyper's providers only.
144
+
145
+
146
+ def _same_url(a: str, b: str) -> bool:
147
+ return (a or "").strip().rstrip("/") == (b or "").strip().rstrip("/")
148
+
149
+
150
+ def _declared_mode(registry, config: TerminalProvider) -> str:
151
+ """Tool gating for an explicitly selected provider: find the live
152
+ models.yaml provider that uses these exact credentials/endpoint and use
153
+ its declared capabilities. No match fails closed to chat-only."""
154
+ for prov in getattr(registry, "providers", {}).values():
155
+ if not prov.live or not prov.api_key or prov.api_key != config.api_key:
156
+ continue
157
+ if config.kind == "anthropic":
158
+ if prov.kind != "anthropic":
159
+ continue
160
+ else:
161
+ if prov.kind not in ("openai_compat", "openai"):
162
+ continue
163
+ if prov.base_url:
164
+ if not _same_url(prov.base_url, config.base_url or ""):
165
+ continue
166
+ elif not (prov.kind == "openai"
167
+ and urlsplit(config.base_url or "").hostname == "api.openai.com"):
168
+ continue
169
+ # Declared AND qualified (spec §11): tool_use is earned by a
170
+ # Hyper-signed qualification record, never by declaration alone.
171
+ caps = prov.effective_capabilities(config.model)
172
+ if "tool_use" in caps:
173
+ return "tools"
174
+ if "chat" in caps:
175
+ return "chat_only"
176
+ return "chat_only"
177
+
178
+
179
+ def _registry_config(prov, model: str, env: Mapping[str, str]) -> TerminalProvider:
180
+ """TerminalProvider for a registry provider + model."""
181
+ kind = "anthropic" if prov.kind == "anthropic" else "openai_compat"
182
+ base_url = prov.base_url or None
183
+ if kind == "anthropic" and env.get("ANTHROPIC_BASE_URL", "").strip():
184
+ base_url = env.get("ANTHROPIC_BASE_URL", "").strip()
185
+ try:
186
+ from hyperclaw.model_policy import model_label
187
+ label = model_label(prov, model)
188
+ except Exception: # noqa: BLE001 -- a label must never break routing
189
+ label = getattr(prov, "display_label", "") or ""
190
+ return TerminalProvider(
191
+ name=prov.name, kind=kind, api_key=prov.api_key, model=model,
192
+ base_url=_validated_url(base_url) if base_url else None,
193
+ label=label,
194
+ )
195
+
196
+
197
+ REGISTRY_ONLY = ("xai", "openai")
198
+
199
+
200
+ def _named_registry_route(registry, env: Mapping[str, str], hyper: bool):
201
+ """LLM_PROVIDER=xai / openai: those providers have no raw-env group in
202
+ resolve_terminal_provider, so they come from the registry (key from
203
+ .env, tools only once qualified). None when LLM_PROVIDER names another."""
204
+ name = env.get("LLM_PROVIDER", "").strip().lower()
205
+ if hyper or name not in REGISTRY_ONLY:
206
+ return None
207
+ prov = getattr(registry, "providers", {}).get(name)
208
+ if prov is None or not prov.live:
209
+ key = getattr(prov, "api_key_env", "") or f"{name.upper()}_API_KEY"
210
+ raise TerminalConfigurationError(
211
+ f"LLM_PROVIDER={name} is selected but {key} is not set. "
212
+ "Existing settings have not been changed.")
213
+ model = prov.model_for()
214
+ caps = prov.effective_capabilities(model)
215
+ return ("tools" if "tool_use" in caps else "chat_only"), \
216
+ _registry_config(prov, model, env), model
217
+
218
+
219
+ def select_terminal_route(registry, env: Mapping[str, str] | None = None) -> tuple:
220
+ """Return ``(mode, TerminalProvider, model)`` for the terminal.
221
+
222
+ mode is "tools" or "chat_only"; ``(None, None, None)`` means nothing usable
223
+ is configured (caller must refuse to run). Raises
224
+ TerminalConfigurationError for an actionable configuration problem.
225
+
226
+ * A model chosen with /model this session (hyperclaw.model_catalog)
227
+ wins; its mode comes from its own declared + qualified capabilities.
228
+ * Hyper install (hyperclaw.deployment): registry only, LLM_PROVIDER and
229
+ raw env groups ignored; nothing live means (None, None, None).
230
+ * LLM_PROVIDER set: honored via resolve_terminal_provider(); tools only
231
+ if the matching models.yaml provider declares tool_use AND it is
232
+ qualified (Provider.effective_capabilities).
233
+ * LLM_PROVIDER=hyperspeed (written by onboarding): the registry's
234
+ HyperSpeed V2 provider; no Hyper access key raises a plain
235
+ missing-key error (never a switch to another provider).
236
+ * LLM_PROVIDER unset: truth_guard.route_chat() over the registry (tool-
237
+ capable provider first). If the registry finds nothing, a complete
238
+ OPENAI_*/HYPERSPEED_* group (incl. the legacy OPENAI_API_BASE alias) is
239
+ still accepted, chat-only unless declared otherwise.
240
+ """
241
+ from hyperclaw.deployment import is_hyper
242
+ from hyperclaw.truth_guard import route_chat
243
+
244
+ env = os.environ if env is None else env
245
+ hyper = is_hyper()
246
+ # /model (docs/MODEL-SWITCHING.md): a model chosen in this session wins.
247
+ # Tools only if that exact provider+model is declared AND qualified.
248
+ from hyperclaw.model_catalog import TERMINAL, selected_route
249
+ chosen = selected_route(registry, TERMINAL)
250
+ if chosen is not None:
251
+ prov, model = chosen
252
+ caps = prov.effective_capabilities(model)
253
+ mode = "tools" if "tool_use" in caps else "chat_only"
254
+ return mode, _registry_config(prov, model, env), model
255
+ # LLM_PROVIDER naming a registry-only provider (xai, openai) on a
256
+ # standard install: that provider, from the registry.
257
+ named = _named_registry_route(registry, env, hyper)
258
+ if named is not None:
259
+ return named
260
+ # A Hyper-deployed install runs on Hyper's providers only (spec §11):
261
+ # LLM_PROVIDER and raw OPENAI_*/ANTHROPIC_* keys are ignored there.
262
+ if hyper or not env.get("LLM_PROVIDER", "").strip():
263
+ mode, prov, model = route_chat(registry)
264
+ if mode is not None:
265
+ return mode, _registry_config(prov, model, env), model
266
+ if hyper:
267
+ return (None, None, None)
268
+ try:
269
+ config = resolve_terminal_provider(env)
270
+ except TerminalConfigurationError:
271
+ return (None, None, None)
272
+ return _declared_mode(registry, config), config, config.model
273
+
274
+ if env.get("LLM_PROVIDER", "").strip().lower() == HYPERSPEED:
275
+ return _hyperspeed_route(registry)
276
+
277
+ config = resolve_terminal_provider(env)
278
+ return _declared_mode(registry, config), config, config.model
279
+
280
+
281
+ HYPERSPEED = "hyperspeed"
282
+ HYPER_KEY_COMMAND = "hyperclaw provider hyper-key"
283
+ SWITCH_TO_OWN_COMMAND = "hyperclaw provider remove hyperspeed"
284
+
285
+
286
+ def hyper_key_missing_message(registry) -> str:
287
+ """What the terminal says when HyperSpeed V2 is selected
288
+ (LLM_PROVIDER=hyperspeed) but no Hyper access key is configured. Names
289
+ the fix, and an own provider that is already configured, without ever
290
+ switching to it."""
291
+ lines = [
292
+ "HyperSpeed V2 is selected (LLM_PROVIDER=hyperspeed in your .env), but your "
293
+ "Hyper access key is missing.",
294
+ f"Add it with: {HYPER_KEY_COMMAND}",
295
+ ]
296
+ own = [p for p in getattr(registry, "providers", {}).values()
297
+ if getattr(p, "live", False) and getattr(p, "origin", "") != "hyper"]
298
+ if own:
299
+ labels = ", ".join(sorted({getattr(p, "display_label", "") or p.name for p in own}))
300
+ lines.append(
301
+ f"You also have {labels} configured. To use it instead of HyperSpeed V2, run: "
302
+ f"{SWITCH_TO_OWN_COMMAND} (this clears HyperSpeed V2 as your default; "
303
+ f"{HYPER_KEY_COMMAND} sets it back).")
304
+ lines.append("Nothing has been changed.")
305
+ return "\n".join(lines)
306
+
307
+
308
+ def _hyperspeed_route(registry) -> tuple:
309
+ """LLM_PROVIDER=hyperspeed (what onboarding writes for HyperSpeed V2):
310
+ the registry's HyperSpeed V2 provider, tools only once qualified. No key
311
+ is an actionable error, never a silent switch to another provider."""
312
+ prov = getattr(registry, "providers", {}).get(HYPERSPEED)
313
+ if prov is None or not prov.live:
314
+ raise TerminalConfigurationError(hyper_key_missing_message(registry))
315
+ model = prov.model_for()
316
+ caps = prov.effective_capabilities(model)
317
+ mode = "tools" if "tool_use" in caps else "chat_only"
318
+ config = TerminalProvider(
319
+ name=prov.name, kind="openai_compat", api_key=prov.api_key, model=model,
320
+ base_url=_validated_url(prov.base_url) if prov.base_url else None,
321
+ label=getattr(prov, "display_label", "") or "",
322
+ )
323
+ return mode, config, model
@@ -0,0 +1 @@
1
+ """HyperClaw tool standard (TOOL-INVENTORY-2026-10-02 §3; docs/TOOL-STANDARD.md)."""