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,60 @@
1
+ """Unattended mode: one switch for everything HyperClaw does on its own.
2
+
3
+ ``HYPERCLAW_UNATTENDED=1`` turns on the scheduled jobs (heartbeat, morning
4
+ brief, memory consolidation, ...) and Telegram polling. Anything else,
5
+ including ``true``, ``yes``, ``0`` or an empty/unset value, leaves them OFF.
6
+ Off is the default (Vaughn's rc3 ruling, 2026-10-09: internal team only,
7
+ overnight and unattended tasks switched off).
8
+
9
+ The interactive terminal (``hyperclaw``, ``python -m hyperclaw tui``) and
10
+ request-driven API endpoints do not depend on this flag.
11
+
12
+ When a background entry point starts with the flag off it logs ONE line
13
+ (``OFF_LINE``) and does nothing: the server keeps serving, a stand-alone
14
+ poller exits with status 0 so launchd (``KeepAlive.SuccessfulExit=false``)
15
+ does not restart it.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import os
21
+ import sys
22
+
23
+ ENV = "HYPERCLAW_UNATTENDED"
24
+
25
+ OFF_LINE = ("Unattended mode is off: scheduled jobs and Telegram polling are not "
26
+ "started. To turn them on, set HYPERCLAW_UNATTENDED=1 (only the exact "
27
+ "value 1 enables it).")
28
+
29
+
30
+ def enabled() -> bool:
31
+ """True only when HYPERCLAW_UNATTENDED is exactly ``1``."""
32
+ return os.environ.get(ENV, "") == "1"
33
+
34
+
35
+ _said = False # one line per process, however many entry points ask
36
+
37
+
38
+ def log_off(logger: logging.Logger | None = None, what: str = "") -> bool:
39
+ """Log the 'unattended mode is off' line, once per process. Returns
40
+ whether it logged now."""
41
+ global _said
42
+ if _said:
43
+ return False
44
+ _said = True
45
+ line = f"{OFF_LINE} ({what})" if what else OFF_LINE
46
+ (logger or logging.getLogger("hyperclaw.unattended")).warning(line)
47
+ return True
48
+
49
+
50
+ def exit_if_off(logger: logging.Logger | None = None, what: str = "") -> None:
51
+ """For stand-alone pollers/daemons: when the flag is off, log the line and
52
+ exit 0 (a clean exit, so a KeepAlive/on-failure supervisor does not
53
+ restart it)."""
54
+ if enabled():
55
+ return
56
+ if logging.getLogger().handlers or (logger is not None and logger.handlers):
57
+ log_off(logger, what)
58
+ else: # logging not configured yet: still say it, once, on stderr
59
+ print(OFF_LINE + (f" ({what})" if what else ""), file=sys.stderr)
60
+ raise SystemExit(0)
@@ -0,0 +1,54 @@
1
+ """HyperClaw terminal updates (docs/UPDATES.md).
2
+
3
+ Entry points call :func:`on_startup` once. It never raises, never blocks on
4
+ the network, and does nothing when ``HYPERCLAW_NO_UPDATE_CHECK=1``.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import sys
10
+
11
+ log = logging.getLogger("hyperclaw.updates")
12
+
13
+ INTERACTIVE = ("cli", "tui")
14
+ _done = False
15
+
16
+
17
+ def startup_lines(surface: str = "cli") -> list:
18
+ """What the startup notice would show (cache + unread owner notices)."""
19
+ from hyperclaw.updates import check, notices
20
+ from hyperclaw.updates.paths import opted_out
21
+ if opted_out():
22
+ return []
23
+ lines = []
24
+ if surface in INTERACTIVE:
25
+ lines += [f"[update] {t}" for t in notices.unread(mark_read=True)]
26
+ n = check.notice_text()
27
+ if n:
28
+ lines.append(n)
29
+ return lines
30
+
31
+
32
+ def on_startup(surface: str = "cli", *, out=None) -> None:
33
+ """Startup hook: show the cached notice, refresh a stale cache in the
34
+ background and, on long-running surfaces, start the managed scheduler
35
+ (only when its gate passes). Once per process."""
36
+ global _done
37
+ if _done:
38
+ return
39
+ _done = True
40
+ try:
41
+ from hyperclaw.updates import check, managed
42
+ from hyperclaw.updates.paths import opted_out
43
+ if opted_out():
44
+ return
45
+ for line in startup_lines(surface):
46
+ if surface in INTERACTIVE:
47
+ print(line, file=out or sys.stderr)
48
+ else:
49
+ log.warning(line)
50
+ check.maybe_refresh_in_background()
51
+ if surface not in INTERACTIVE:
52
+ managed.start_scheduler()
53
+ except Exception as e: # never let the updater break startup
54
+ log.debug("update startup hook: %s", e)
@@ -0,0 +1,131 @@
1
+ """Cached, rate-limited, offline-safe version check + the startup notice.
2
+
3
+ Startup only ever READS the cache. A stale cache is refreshed in a daemon
4
+ thread (short timeouts), so the notice for a new release appears on the next
5
+ start. A version is only advertised once its signed manifest verifies.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import datetime as _dt
10
+ import logging
11
+ import threading
12
+ from typing import Optional
13
+
14
+ from hyperclaw import distribution as _dist
15
+ from hyperclaw.updates import index, versioning
16
+ from hyperclaw.updates.manifest import ManifestError
17
+ from hyperclaw.updates.paths import (current_version, opted_out, read_json, updates_dir,
18
+ write_json)
19
+
20
+ log = logging.getLogger("hyperclaw.updates")
21
+
22
+ CHECK_INTERVAL = _dt.timedelta(hours=24)
23
+ FAIL_BACKOFF = _dt.timedelta(hours=6)
24
+ TIMEOUT = 4.0
25
+
26
+
27
+ def _now() -> _dt.datetime:
28
+ return _dt.datetime.now(_dt.timezone.utc)
29
+
30
+
31
+ def cache_path():
32
+ return updates_dir() / "check.json"
33
+
34
+
35
+ def load_cache() -> dict:
36
+ c = read_json(cache_path(), {})
37
+ return c if isinstance(c, dict) else {}
38
+
39
+
40
+ def _ts(s) -> Optional[_dt.datetime]:
41
+ try:
42
+ t = _dt.datetime.fromisoformat(str(s))
43
+ return t if t.tzinfo else t.replace(tzinfo=_dt.timezone.utc)
44
+ except Exception:
45
+ return None
46
+
47
+
48
+ def is_due(cache: dict, now: Optional[_dt.datetime] = None) -> bool:
49
+ now = now or _now()
50
+ last = _ts(cache.get("checked_at"))
51
+ if last is None:
52
+ return True
53
+ wait = FAIL_BACKOFF if cache.get("error") else CHECK_INTERVAL
54
+ return now - last >= wait or last > now + _dt.timedelta(minutes=5)
55
+
56
+
57
+ def refresh(now: Optional[_dt.datetime] = None, *, timeout: float = TIMEOUT,
58
+ current: Optional[str] = None) -> dict:
59
+ """Ask the index for the newest release and verify its signed manifest.
60
+ Never raises; failures are recorded in the cache (and back off)."""
61
+ now = now or _now()
62
+ current = current or current_version()
63
+ cache = load_cache()
64
+ cache.update({"checked_at": now.isoformat(), "current": current})
65
+ try:
66
+ info = index.project_info(timeout=timeout)
67
+ latest = index.latest_version(info, include_pre=index.include_prereleases(current))
68
+ cache["latest"] = latest
69
+ cache.pop("error", None)
70
+ if latest and versioning.is_newer(latest, current):
71
+ try:
72
+ rel = index.release_info(latest, timeout=timeout)
73
+ m = index.fetch_manifest(latest, rel, timeout=timeout)
74
+ cache["latest_signed"] = m["version"]
75
+ cache["min_upgrade_from"] = m["min_upgrade_from"]
76
+ cache.pop("unsigned", None)
77
+ except (ManifestError, index.IndexError_) as e:
78
+ cache["unsigned"] = latest
79
+ cache.pop("latest_signed", None)
80
+ log.info("update check: %s is not installable: %s", latest, e)
81
+ else:
82
+ cache.pop("latest_signed", None)
83
+ cache.pop("unsigned", None)
84
+ except Exception as e: # offline, DNS, bad JSON, anything: never raise
85
+ cache["error"] = f"{type(e).__name__}"
86
+ log.debug("update check failed: %s", e)
87
+ try:
88
+ write_json(cache_path(), cache)
89
+ except Exception:
90
+ pass
91
+ return cache
92
+
93
+
94
+ def notice_text(cache: Optional[dict] = None, current: Optional[str] = None,
95
+ legacy: Optional[bool] = None) -> Optional[str]:
96
+ """The one-line notice, or None. Reads the cache only (no network)."""
97
+ if opted_out():
98
+ return None
99
+ cache = load_cache() if cache is None else cache
100
+ current = current or current_version()
101
+ if _dist.is_legacy_install() if legacy is None else legacy:
102
+ # Old ``hyperclaw`` distribution: the only notice is how to move over.
103
+ latest = cache.get("latest")
104
+ if not latest or not versioning.is_newer(latest, current):
105
+ latest = current
106
+ return _dist.migration_line(latest)
107
+ latest = cache.get("latest_signed")
108
+ if not latest or not versioning.is_newer(latest, current):
109
+ return None
110
+ mn = versioning.parse(cache.get("min_upgrade_from") or "0")
111
+ cur = versioning.parse(current)
112
+ if mn and cur and cur < mn:
113
+ return (f"HyperClaw {latest} is available (you have {current}); upgrade to "
114
+ f"{cache.get('min_upgrade_from')} first. Run 'hyperclaw upgrade'.")
115
+ return (f"HyperClaw {latest} is available (you have {current}). "
116
+ f"Run 'hyperclaw upgrade' to see what's new.")
117
+
118
+
119
+ _thread: Optional[threading.Thread] = None
120
+
121
+
122
+ def maybe_refresh_in_background(now: Optional[_dt.datetime] = None) -> Optional[threading.Thread]:
123
+ """Start one daemon refresh if the cache is stale. Returns the thread."""
124
+ global _thread
125
+ if opted_out() or not is_due(load_cache(), now):
126
+ return None
127
+ if _thread is not None and _thread.is_alive():
128
+ return None
129
+ _thread = threading.Thread(target=refresh, name="hyperclaw-update-check", daemon=True)
130
+ _thread.start()
131
+ return _thread
@@ -0,0 +1,178 @@
1
+ """Talking to the package index (PyPI JSON API) and fetching signed manifests.
2
+
3
+ Only ``https`` URLs are fetched, plus plain ``http`` to a loopback host (a
4
+ local mirror or the unit tests' fake index). Every call has a timeout and a
5
+ size cap. Nothing here installs anything.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import urllib.parse
13
+ import urllib.request
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from hyperclaw.updates import manifest as _m
18
+ from hyperclaw.updates import versioning
19
+
20
+ DEFAULT_INDEX = "https://pypi.org/pypi"
21
+ INDEX_ENV = "HYPERCLAW_UPDATE_INDEX_URL"
22
+ MANIFEST_URL_ENV = "HYPERCLAW_UPDATE_MANIFEST_URL"
23
+ PRERELEASE_ENV = "HYPERCLAW_UPDATE_PRERELEASE"
24
+ MANIFEST_URL_KEYS = ("release-manifest", "release manifest", "release_manifest")
25
+
26
+ JSON_CAP = 8 << 20
27
+ WHEEL_CAP = 200 << 20
28
+ _LOOPBACK = {"127.0.0.1", "localhost", "::1"}
29
+
30
+
31
+ class IndexError_(Exception):
32
+ """Index unreachable or answered something unusable."""
33
+
34
+
35
+ def index_url() -> str:
36
+ return (os.environ.get(INDEX_ENV, "").strip() or DEFAULT_INDEX).rstrip("/")
37
+
38
+
39
+ def _check_url(url: str) -> str:
40
+ u = urllib.parse.urlparse(url)
41
+ if u.scheme == "https" and u.hostname:
42
+ return url
43
+ if u.scheme == "http" and (u.hostname or "") in _LOOPBACK:
44
+ return url
45
+ raise IndexError_(f"refusing non-https URL ({u.scheme}://{u.hostname})")
46
+
47
+
48
+ def _open(url: str, timeout: float):
49
+ req = urllib.request.Request(_check_url(url), headers={
50
+ "User-Agent": "hyperclaw-updater", "Accept": "application/json, */*"})
51
+ return urllib.request.urlopen(req, timeout=timeout) # noqa: S310 (scheme checked)
52
+
53
+
54
+ def fetch_bytes(url: str, *, timeout: float = 10, cap: int = JSON_CAP) -> bytes:
55
+ try:
56
+ with _open(url, timeout) as r:
57
+ data = r.read(cap + 1)
58
+ except IndexError_:
59
+ raise
60
+ except Exception as e:
61
+ raise IndexError_(f"fetch failed: {type(e).__name__}")
62
+ if len(data) > cap:
63
+ raise IndexError_("response too large")
64
+ return data
65
+
66
+
67
+ def fetch_json(url: str, *, timeout: float = 10):
68
+ try:
69
+ return json.loads(fetch_bytes(url, timeout=timeout).decode("utf-8"))
70
+ except IndexError_:
71
+ raise
72
+ except Exception:
73
+ raise IndexError_("index returned invalid JSON")
74
+
75
+
76
+ def project_info(*, timeout: float = 10) -> dict:
77
+ return fetch_json(f"{index_url()}/{_m.PACKAGE}/json", timeout=timeout)
78
+
79
+
80
+ def release_info(version: str, *, timeout: float = 10) -> dict:
81
+ return fetch_json(f"{index_url()}/{_m.PACKAGE}/{versioning.normalize(version)}/json",
82
+ timeout=timeout)
83
+
84
+
85
+ def include_prereleases(current: str) -> bool:
86
+ if os.environ.get(PRERELEASE_ENV, "").strip() == "1":
87
+ return True
88
+ v = versioning.parse(current)
89
+ return bool(v and v.is_prerelease)
90
+
91
+
92
+ def latest_version(info: dict, *, include_pre: bool) -> Optional[str]:
93
+ """Highest non-yanked release on the index (prereleases only if asked)."""
94
+ best = None
95
+ for ver, files in (info.get("releases") or {}).items():
96
+ v = versioning.parse(ver)
97
+ if v is None or v.local or (v.is_prerelease and not include_pre):
98
+ continue
99
+ if isinstance(files, list) and files and all(f.get("yanked") for f in files):
100
+ continue
101
+ if best is None or v > best:
102
+ best = v
103
+ return str(best) if best else None
104
+
105
+
106
+ def manifest_url_for(version: str, release_json: Optional[dict]) -> Optional[str]:
107
+ """The release's manifest URL: env template, else the release's own
108
+ ``Release-Manifest`` project URL. ``{version}`` is substituted."""
109
+ tpl = os.environ.get(MANIFEST_URL_ENV, "").strip()
110
+ if not tpl and isinstance(release_json, dict):
111
+ urls = (release_json.get("info") or {}).get("project_urls") or {}
112
+ for k, val in urls.items():
113
+ if str(k).strip().lower() in MANIFEST_URL_KEYS and val:
114
+ tpl = str(val)
115
+ break
116
+ if not tpl:
117
+ return None
118
+ return tpl.replace("{version}", versioning.normalize(version))
119
+
120
+
121
+ def fetch_manifest(version: str, release_json: Optional[dict] = None, *,
122
+ timeout: float = 10) -> dict:
123
+ """Fetch and VERIFY the signed manifest for ``version``.
124
+ Raises ManifestError when there is none or it does not verify."""
125
+ return fetch_signed(version, release_json, timeout=timeout)[0]
126
+
127
+
128
+ def fetch_signed(version: str, release_json: Optional[dict] = None, *,
129
+ timeout: float = 10):
130
+ """(verified manifest, signed envelope) for ``version``; ManifestError otherwise."""
131
+ url = manifest_url_for(version, release_json)
132
+ if not url:
133
+ raise _m.ManifestError(f"release {version} has no signed manifest (unsigned release)")
134
+ try:
135
+ env = fetch_json(url, timeout=timeout)
136
+ except IndexError_ as e:
137
+ raise _m.ManifestError(f"signed manifest for {version} unavailable ({e})")
138
+ return _m.verify_manifest(env, expect_version=version), env
139
+
140
+
141
+ def wheel_url(release_json: dict, filename: str) -> str:
142
+ for f in release_json.get("urls") or []:
143
+ if f.get("filename") == filename:
144
+ if f.get("yanked"):
145
+ raise IndexError_(f"{filename} is yanked")
146
+ return f["url"]
147
+ raise IndexError_(f"{filename} is not on the index")
148
+
149
+
150
+ def download_wheel(url: str, dest_dir: Path, manifest: dict, *, timeout: float = 60) -> Path:
151
+ """Download into ``dest_dir`` and check size + sha256 against the signed
152
+ manifest. A mismatching file is deleted and ManifestError raised."""
153
+ dest_dir.mkdir(parents=True, exist_ok=True)
154
+ dest = dest_dir / manifest["wheel"]
155
+ part = dest.with_suffix(".part")
156
+ h, n = hashlib.sha256(), 0
157
+ try:
158
+ with _open(url, timeout) as r, open(part, "wb") as out:
159
+ while True:
160
+ chunk = r.read(1 << 16)
161
+ if not chunk:
162
+ break
163
+ n += len(chunk)
164
+ if n > min(WHEEL_CAP, manifest["size"]):
165
+ raise _m.ManifestError("wheel is larger than the signed manifest says")
166
+ h.update(chunk)
167
+ out.write(chunk)
168
+ except _m.ManifestError:
169
+ part.unlink(missing_ok=True)
170
+ raise
171
+ except Exception as e:
172
+ part.unlink(missing_ok=True)
173
+ raise IndexError_(f"wheel download failed: {type(e).__name__}")
174
+ if n != manifest["size"] or h.hexdigest() != manifest["sha256"]:
175
+ part.unlink(missing_ok=True)
176
+ raise _m.ManifestError("wheel sha256 does not match the signed manifest")
177
+ os.replace(part, dest)
178
+ return dest
@@ -0,0 +1,110 @@
1
+ """Install-method detection, the pip/pipx call, and the post-install check.
2
+
3
+ Supported: a pipx install (``.../pipx/venvs/<name>``) and a plain venv. Any
4
+ other interpreter (system Python, container image) is refused with
5
+ instructions; the updater never pip-installs into a system Python.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Callable, List, Optional, Tuple
17
+
18
+ from hyperclaw.updates import versioning
19
+
20
+ UNSUPPORTED = (
21
+ "This HyperClaw is not installed with pipx or in a virtualenv, so "
22
+ "'hyperclaw upgrade' cannot update it safely.\n"
23
+ "Reinstall with pipx (recommended): pipx install hyperagents\n"
24
+ "or, in a container, rebuild the image with the new version.")
25
+
26
+
27
+ class UnsupportedInstall(Exception):
28
+ pass
29
+
30
+
31
+ @dataclass
32
+ class InstallMethod:
33
+ kind: str # "pipx" | "venv"
34
+ prefix: Path # the environment HyperClaw runs from
35
+ pip: List[str] # command prefix that runs pip in that env
36
+ name: str = "" # pipx venv name
37
+
38
+ @property
39
+ def bin_dir(self) -> Path:
40
+ return self.prefix / ("Scripts" if os.name == "nt" else "bin")
41
+
42
+ def hyperclaw_exe(self) -> Path:
43
+ return self.bin_dir / ("hyperclaw.exe" if os.name == "nt" else "hyperclaw")
44
+
45
+
46
+ def _pipx_venvs_roots(env) -> List[Path]:
47
+ roots = []
48
+ if env.get("PIPX_HOME"):
49
+ roots.append(Path(env["PIPX_HOME"]) / "venvs")
50
+ home = Path(env.get("HOME") or Path.home())
51
+ roots += [home / ".local" / "pipx" / "venvs", home / ".local" / "share" / "pipx" / "venvs"]
52
+ return roots
53
+
54
+
55
+ def detect(prefix: Optional[str] = None, base_prefix: Optional[str] = None,
56
+ env=None) -> InstallMethod:
57
+ env = os.environ if env is None else env
58
+ pfx = Path(prefix or sys.prefix)
59
+ base = Path(base_prefix or sys.base_prefix)
60
+ parent = pfx.parent
61
+ is_pipx = parent.name == "venvs" and (
62
+ parent.parent.name == "pipx" or any(parent == r for r in _pipx_venvs_roots(env)))
63
+ if is_pipx:
64
+ pipx = shutil.which("pipx", path=env.get("PATH"))
65
+ if not pipx:
66
+ raise UnsupportedInstall(
67
+ "HyperClaw is installed with pipx, but the 'pipx' command is not on PATH. "
68
+ "Add it to PATH and run 'hyperclaw upgrade' again.")
69
+ return InstallMethod("pipx", pfx, [pipx, "runpip", pfx.name], name=pfx.name)
70
+ if pfx.resolve() != base.resolve():
71
+ py = pfx / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
72
+ return InstallMethod("venv", pfx, [str(py), "-m", "pip"])
73
+ raise UnsupportedInstall(UNSUPPORTED)
74
+
75
+
76
+ Runner = Callable[..., "subprocess.CompletedProcess"]
77
+
78
+
79
+ def install(method: InstallMethod, wheel: Path, *, force: bool = False,
80
+ runner: Runner = subprocess.run) -> Tuple[bool, str]:
81
+ cmd = list(method.pip) + ["install", "--disable-pip-version-check", "-q"]
82
+ if force:
83
+ cmd += ["--force-reinstall", "--no-deps"]
84
+ cmd.append(str(wheel))
85
+ try:
86
+ r = runner(cmd, capture_output=True, text=True, timeout=900, check=False)
87
+ except Exception as e:
88
+ return False, f"{type(e).__name__}: {e}"
89
+ return r.returncode == 0, (r.stdout or "") + (r.stderr or "")
90
+
91
+
92
+ _VER = re.compile(r"\bv?(\d+(?:\.\d+)+\S*)")
93
+
94
+
95
+ def installed_version(method: InstallMethod, *, runner: Runner = subprocess.run) -> Optional[str]:
96
+ """Run ``hyperclaw version`` in the target env and parse the version."""
97
+ try:
98
+ r = runner([str(method.hyperclaw_exe()), "version"], capture_output=True,
99
+ text=True, timeout=120, check=False)
100
+ except Exception:
101
+ return None
102
+ if r.returncode != 0:
103
+ return None
104
+ m = _VER.search(re.sub(r"\x1b\[[0-9;]*m", "", r.stdout or ""))
105
+ return m.group(1) if m else None
106
+
107
+
108
+ def verify(method: InstallMethod, expect: str, *, runner: Runner = subprocess.run) -> bool:
109
+ got = installed_version(method, runner=runner)
110
+ return bool(got) and versioning.parse(got) == versioning.parse(expect)