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,211 @@
1
+ """Model policy for HyperAgents (Vaughn, 2026-10-12; docs/MODEL-POLICY.md).
2
+
3
+ - HyperSpeed V2 is the default and recommended model. HyperAgents may also run
4
+ Claude, OpenAI or Grok (xAI) models, on every install; a Hyper-deployed
5
+ install only the models on its allowed list (hyperclaw.managed_config).
6
+ - Labels: Hyper's models by name ("HyperSpeed V2"); a model on a key Hyper
7
+ issued "<model name>, via Hyper"; a model on a key the user added
8
+ "your own provider (<provider>)".
9
+ - Consent: choosing a third-party model shows exactly one line,
10
+ CONSENT_LINE, and needs the owner's yes, recorded (time, provider, model)
11
+ in an owner-only file. No consent, no switch, and no automatic fallback to
12
+ that provider.
13
+ - Honesty: which model is answering is said at start-up, in /model, on every
14
+ switch and on every automatic fallback (one line naming the new model and
15
+ why).
16
+ - Keys: a key that looks like a RunPod key (rpa_) is never accepted for any
17
+ provider.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import datetime as _dt
22
+ import json
23
+ import logging
24
+ import os
25
+ import re
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+ log = logging.getLogger("hyperclaw.model_policy")
30
+
31
+ CONSENT_LINE = "Messages, emails and documents you share will be processed by {provider}."
32
+ CONSENT_FILE = "model_consent.json"
33
+
34
+ # The provider named in the consent line.
35
+ PROVIDER_NAMES = {
36
+ "anthropic": "Anthropic",
37
+ "anthropic_compat": "the Anthropic-compatible endpoint you configured",
38
+ "openai": "OpenAI",
39
+ "openai_default": "the OpenAI-compatible endpoint you configured",
40
+ "openai_compat": "the OpenAI-compatible endpoint you configured",
41
+ "xai": "xAI",
42
+ "bedrock": "AWS Bedrock",
43
+ }
44
+
45
+ RUNPOD_PREFIX = "rpa_"
46
+ RUNPOD_REFUSAL = ("That key can't be used here: it isn't a model provider key or a Hyper "
47
+ "access key. Nothing was saved.")
48
+
49
+
50
+ # ───────────────────────── who is third-party ─────────────────────────
51
+
52
+ def is_third_party(prov) -> bool:
53
+ return getattr(prov, "origin", "customer") != "hyper"
54
+
55
+
56
+ def provider_name(prov_or_name) -> str:
57
+ name = prov_or_name if isinstance(prov_or_name, str) else getattr(prov_or_name, "name", "")
58
+ if name in PROVIDER_NAMES:
59
+ return PROVIDER_NAMES[name]
60
+ label = "" if isinstance(prov_or_name, str) else getattr(prov_or_name, "label", "")
61
+ return label or name
62
+
63
+
64
+ def consent_line(prov_or_name) -> str:
65
+ return CONSENT_LINE.format(provider=provider_name(prov_or_name))
66
+
67
+
68
+ # ───────────────────────── labels ─────────────────────────
69
+
70
+ _GPT = re.compile(r"^gpt-", re.I)
71
+
72
+
73
+ def pretty_model_name(provider: str, model: str) -> str:
74
+ """A readable model name: managed name, known name, else from the id
75
+ ("claude-sonnet-5-5" -> "Claude Sonnet 5.5")."""
76
+ from hyperclaw import managed_config, xai
77
+ named = managed_config.model_name(provider, model)
78
+ if named:
79
+ return named
80
+ if model in xai.NAMES:
81
+ return xai.NAMES[model]
82
+ if not model:
83
+ return provider
84
+ if _GPT.match(model):
85
+ return "GPT-" + model[4:]
86
+ words, nums = [], []
87
+ for part in model.split("-"):
88
+ if part.isdigit() and len(part) <= 2:
89
+ nums.append(part)
90
+ elif nums:
91
+ break # a date or snapshot suffix after the version: stop
92
+ else:
93
+ words.append(part[:1].upper() + part[1:])
94
+ return " ".join(words + ([".".join(nums)] if nums else [])) or model
95
+
96
+
97
+ def model_label(prov, model: str) -> str:
98
+ """How this provider + model is named to the user."""
99
+ from hyperclaw import managed_config
100
+ if not is_third_party(prov):
101
+ return getattr(prov, "display_label", "") or getattr(prov, "label", "") or prov.name
102
+ if managed_config.hyper_issued(prov.name):
103
+ return f"{pretty_model_name(prov.name, model)}, via Hyper"
104
+ return f"your own provider ({getattr(prov, 'label', '') or prov.name})"
105
+
106
+
107
+ def answering_text(label: str, model: str) -> str:
108
+ """'Answering: HyperSpeed V2 · model hospitality'."""
109
+ return f"Answering: {label}" + (f" · model {model}" if model else "")
110
+
111
+
112
+ def fallback_notice(new_label: str, new_model: str, old_label: str) -> str:
113
+ """One line when an automatic fallback changes the answering model."""
114
+ model = f" · model {new_model}" if new_model else ""
115
+ return f"Now answering: {new_label}{model}, because {old_label} didn't respond."
116
+
117
+
118
+ # ───────────────────────── consent (owner-only file) ─────────────────────────
119
+
120
+ def consent_path() -> Path:
121
+ from hyperclaw.userconfig import root
122
+ return root() / "config" / CONSENT_FILE
123
+
124
+
125
+ def _read() -> list:
126
+ try:
127
+ data = json.loads(consent_path().read_text(encoding="utf-8"))
128
+ rows = data.get("consents") if isinstance(data, dict) else None
129
+ return [r for r in rows if isinstance(r, dict)] if isinstance(rows, list) else []
130
+ except Exception: # noqa: BLE001 -- missing or unreadable: no consent
131
+ return []
132
+
133
+
134
+ def has_consent(provider: str) -> bool:
135
+ """True when the owner agreed to this provider processing their data."""
136
+ return any(r.get("provider") == provider for r in _read())
137
+
138
+
139
+ def consents() -> list:
140
+ return _read()
141
+
142
+
143
+ def record_consent(provider: str, model: str) -> dict:
144
+ """Append the owner's consent (UTC time, provider, model). The file is
145
+ owner read/write only (0600)."""
146
+ row = {"provider": provider, "model": model,
147
+ "provider_name": provider_name(provider),
148
+ "line": consent_line(provider),
149
+ "at": _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}
150
+ rows = _read() + [row]
151
+ p = consent_path()
152
+ p.parent.mkdir(parents=True, exist_ok=True)
153
+ fd = os.open(str(p), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
154
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
155
+ json.dump({"consents": rows}, f, indent=1)
156
+ try:
157
+ os.chmod(p, 0o600)
158
+ except OSError:
159
+ pass
160
+ return row
161
+
162
+
163
+ def needs_consent(prov) -> bool:
164
+ return is_third_party(prov) and not has_consent(prov.name)
165
+
166
+
167
+ def fallback_ladder(candidates: list) -> list:
168
+ """An automatic fallback never moves to a third-party provider the owner
169
+ hasn't agreed to; the first rung (the configured or chosen route) stays."""
170
+ if not candidates:
171
+ return []
172
+ return [candidates[0]] + [(p, m) for p, m in candidates[1:] if not needs_consent(p)]
173
+
174
+
175
+ # ───────────────────────── keys ─────────────────────────
176
+
177
+ def looks_like_runpod_key(key: str) -> bool:
178
+ return (key or "").strip().lower().startswith(RUNPOD_PREFIX)
179
+
180
+
181
+ def check_key(key: str) -> str:
182
+ """'' when the key may be saved; else the plain reason it is refused."""
183
+ if looks_like_runpod_key(key):
184
+ return RUNPOD_REFUSAL
185
+ return ""
186
+
187
+
188
+ # ───────────────────────── the allowed list, for people ─────────────────────────
189
+
190
+ def allowed_display(entries: Optional[list] = None) -> list:
191
+ """The allowed list as names, HyperSpeed V2 first (onboarding, doctor,
192
+ provider list). [] when the install is not restricted."""
193
+ from hyperclaw import hyperspeed, managed_config
194
+ if entries is None:
195
+ entries = managed_config.allowed_models() or []
196
+ hyper, other = [], []
197
+ for e in entries:
198
+ prov, _, model = e.partition("/")
199
+ if prov == hyperspeed.PROVIDER_NAME:
200
+ if model in ("*", hyperspeed.model()):
201
+ hyper.insert(0, f"{hyperspeed.LABEL} (recommended)")
202
+ if model == "*":
203
+ hyper.append("other Hyper models")
204
+ elif model != hyperspeed.model():
205
+ hyper.append(f"{pretty_model_name(prov, model)} (Hyper)")
206
+ continue
207
+ base = (pretty_model_name(prov, model) if model != "*"
208
+ else f"{provider_name(prov)} models")
209
+ other.append(f"{base}, via Hyper" if managed_config.hyper_issued(prov)
210
+ else f"{base} (your own provider)")
211
+ return list(dict.fromkeys(hyper + other))