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,188 @@
1
+ """What is actually live on this install, for onboarding and help text.
2
+
3
+ The truth rule: the product never claims anything that isn't live on that
4
+ install. Onboarding's welcome and integrations text is generated from this
5
+ snapshot, never from a fixed list of things the product might connect to
6
+ (tests/unit/test_onboarding_live_state.py enforces it).
7
+
8
+ Sources (no network):
9
+ - models: live providers in the registry (keys present, on the allowed list
10
+ for a Hyper-deployed install), with tools vs chat-only from signed
11
+ qualification records only;
12
+ - integrations: Telegram (bot token and chat id set), Gmail and Calendar
13
+ (Google credentials present: hyperclaw.api_utils.gmail_configured);
14
+ - available to connect: Telegram, only when the install profile allows it
15
+ (managed config ``"telegram": true``: team and client installs) and it is
16
+ not connected yet;
17
+ - memory: the customer's database when DATABASE_URL is set, else local files.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from dataclasses import dataclass, field
23
+
24
+
25
+ @dataclass
26
+ class LiveModel:
27
+ label: str
28
+ model: str
29
+ tools: bool
30
+
31
+
32
+ @dataclass
33
+ class LiveState:
34
+ hyper_deployed: bool = False
35
+ models: list = field(default_factory=list) # LiveModel
36
+ integrations: list = field(default_factory=list) # names, e.g. "Telegram"
37
+ available: list = field(default_factory=list) # can be connected here (profile)
38
+ memory: str = "local" # local | database
39
+ allowed: list = field(default_factory=list) # allowed-list names (Hyper-deployed)
40
+
41
+ @property
42
+ def tools(self) -> bool:
43
+ return any(m.tools for m in self.models)
44
+
45
+
46
+ def _models() -> list:
47
+ from hyperclaw.managed_config import is_allowed
48
+ from hyperclaw.model_policy import model_label
49
+ from hyperclaw.providers import registry
50
+ out = []
51
+ try:
52
+ reg = registry()
53
+ except Exception: # noqa: BLE001 -- no config yet: nothing is live
54
+ return out
55
+ for prov in reg.providers.values():
56
+ if not prov.live or "chat" not in prov.capabilities:
57
+ continue
58
+ model = prov.model_for()
59
+ if not model or not is_allowed(prov.name, model):
60
+ continue
61
+ out.append(LiveModel(model_label(prov, model), model,
62
+ "tool_use" in prov.effective_capabilities(model)))
63
+ out.sort(key=lambda m: 0 if m.label.startswith("HyperSpeed") else 1)
64
+ return out
65
+
66
+
67
+ def _integrations() -> list:
68
+ env = os.environ
69
+ out = []
70
+ if env.get("TELEGRAM_BOT_TOKEN", "").strip() and env.get("TELEGRAM_CHAT_ID", "").strip():
71
+ out.append("Telegram")
72
+ try:
73
+ from hyperclaw.api_utils import gmail_configured
74
+ from hyperclaw.userconfig import root
75
+ r = root()
76
+ gmail = gmail_configured() or (r / "workspace" / "secrets" / "gmail_token.json").exists() \
77
+ or (r / "secrets" / "gmail_token.json").exists()
78
+ except Exception: # noqa: BLE001
79
+ gmail = False
80
+ workspace_reads = []
81
+ try: # the Google Workspace sign-in (hyperclaw/google_workspace/auth.py)
82
+ from hyperclaw.google_workspace import auth as gauth
83
+ if gauth.connected():
84
+ granted = set(gauth.granted_scopes())
85
+ gmail = gmail or (gauth.GMAIL_READ in granted and gauth.CALENDAR_READ in granted)
86
+ workspace_reads = [label for scope, label in (
87
+ (gauth.DRIVE_READ, "Drive"), (gauth.DOCS_READ, "Docs"),
88
+ (gauth.SHEETS_READ, "Sheets"), (gauth.SLIDES_READ, "Slides"),
89
+ (gauth.MEET_READ, "Meet")) if scope in granted]
90
+ except Exception: # noqa: BLE001
91
+ workspace_reads = []
92
+ if gmail:
93
+ out.append("Gmail and Calendar")
94
+ if workspace_reads:
95
+ out.append("Google " + ", ".join(workspace_reads[:-1])
96
+ + (" and " if len(workspace_reads) > 1 else "") + workspace_reads[-1]
97
+ + " (read)")
98
+ return out
99
+
100
+
101
+ def _available(connected: list) -> list:
102
+ """Integrations this install's profile lets the owner connect now."""
103
+ try:
104
+ from hyperclaw.managed_config import telegram_allowed
105
+ tg = telegram_allowed()
106
+ except Exception: # noqa: BLE001 -- unreadable profile: offer nothing
107
+ tg = False
108
+ return ["Telegram"] if tg and "Telegram" not in connected else []
109
+
110
+
111
+ def snapshot() -> LiveState:
112
+ from hyperclaw.deployment import is_hyper
113
+ from hyperclaw.model_policy import allowed_display
114
+ hyper = is_hyper()
115
+ connected = _integrations()
116
+ return LiveState(
117
+ hyper_deployed=hyper,
118
+ models=_models(),
119
+ integrations=connected,
120
+ available=_available(connected),
121
+ memory="database" if os.environ.get("DATABASE_URL", "").strip() and not hyper
122
+ else "local",
123
+ allowed=allowed_display() if hyper else [],
124
+ )
125
+
126
+
127
+ # ───────────────────────── text ─────────────────────────
128
+
129
+ PRODUCT = "HyperAgents"
130
+ TOOLS_OFF = ("Tools are off: until a model passes {p}' tool qualification it can only "
131
+ "chat. It can't read or send email, open your files or act in other apps.")
132
+
133
+
134
+ def welcome_lines(state: LiveState) -> list:
135
+ """The welcome, generated from what is live. Plain text (no markup)."""
136
+ lines = [f"Welcome to {PRODUCT}.", ""]
137
+ lines.append(f"{PRODUCT} is an AI assistant you talk to here in the terminal.")
138
+ lines.append("")
139
+ lines.append("On this install right now:")
140
+ if state.models:
141
+ for m in state.models:
142
+ mode = "tools" if m.tools else "chat-only"
143
+ lines.append(f" • Model: {m.label} ({m.model}), {mode}")
144
+ else:
145
+ lines.append(" • Model: none set up yet (setup adds one next)")
146
+ if not state.tools:
147
+ lines.append(" • " + TOOLS_OFF.format(p=PRODUCT))
148
+ if state.integrations:
149
+ lines.append(" • Connected: " + ", ".join(state.integrations))
150
+ else:
151
+ lines.append(" • Connected: nothing yet")
152
+ lines.append(" • Memory: " + ("your database" if state.memory == "database"
153
+ else "kept on this computer"))
154
+ return lines
155
+
156
+
157
+ def steps(state: LiveState) -> list:
158
+ """The setup steps this install will actually go through."""
159
+ if state.hyper_deployed:
160
+ return ["Name your assistant", "Your name", "Your Hyper access key"]
161
+ return ["Name your assistant", "Your name",
162
+ "The model: HyperSpeed V2 (recommended) or your own provider",
163
+ "Optional: a database for memory across sessions"]
164
+
165
+
166
+ def explain_lines(state: LiveState) -> list:
167
+ """What the assistant can do on this install, said plainly."""
168
+ if state.tools:
169
+ names = ", ".join(state.integrations) if state.integrations else "nothing yet"
170
+ return ["I can chat with you and use the tools this install has enabled.",
171
+ f"Connected services: {names}."]
172
+ return ["For now I can chat: answer questions, and draft, rewrite or summarize "
173
+ "what you paste in.",
174
+ "I can't act in your email, files or other apps until a model that has passed "
175
+ f"{PRODUCT}' tool qualification is set up."]
176
+
177
+
178
+ def integrations_lines(state: LiveState) -> list:
179
+ if not state.integrations:
180
+ lines = ["No integrations are connected on this install yet."]
181
+ else:
182
+ lines = ["Connected on this install: " + ", ".join(state.integrations) + "."]
183
+ if state.available:
184
+ lines.append("Available to connect on this install: " + ", ".join(state.available) + ".")
185
+ if not state.tools and "Gmail and Calendar" in state.integrations:
186
+ lines.append("Gmail and Calendar need tools, which are off on this install.")
187
+ return lines
188
+
@@ -0,0 +1,245 @@
1
+ """Managed config: the settings Hyper sets for an install (docs/MODEL-POLICY.md).
2
+
3
+ A small JSON file that the app only ever READS. There is no command, prompt or
4
+ settings screen that writes it, so a user cannot change it through the app.
5
+ Hyper's deployment tooling places it; on a Hyper-deployed install it lives in
6
+ a system location the user's account normally cannot write.
7
+
8
+ Location (first that is set wins):
9
+ 1. ``HYPERCLAW_MANAGED_CONFIG`` (a path; set by Hyper's deployment, e.g. in
10
+ the launchd plist)
11
+ 2. macOS: ``/Library/Application Support/HyperClaw/managed.json``
12
+ Linux and others: ``/etc/hyperclaw/managed.json``
13
+
14
+ Format (schema 1)::
15
+
16
+ {
17
+ "schema": 1,
18
+ "allowed_models": ["hyperspeed/hospitality", "anthropic/claude-sonnet-5-5",
19
+ "xai/grok-4.7"],
20
+ "hyper_issued_keys": ["anthropic", "xai"],
21
+ "model_names": {"anthropic/claude-sonnet-5-5": "Claude Sonnet 5.5"},
22
+ "telegram": true
23
+ }
24
+
25
+ - ``allowed_models``: per-customer allowed-model list for a Hyper-deployed
26
+ install. Entries are ``<provider>/<model id>`` or ``<provider>/*`` (every
27
+ model of that provider). A model not on the list cannot be selected or
28
+ routed to. Missing, empty or unreadable: the default list, HyperSpeed V2
29
+ only. Standard installs are not restricted by it.
30
+ - ``hyper_issued_keys``: providers whose key on this install Hyper issued.
31
+ Their models are labelled "<model name>, via Hyper"; every other
32
+ third-party key is "your own provider".
33
+ - ``model_names``: optional display names, ``<provider>/<model id>`` -> name.
34
+ - ``telegram``: install profile. ``true`` only on installs where Hyper has
35
+ set up the Telegram channel to run (team and client profiles: their own
36
+ bot, unattended mode on for the main service). Only then does onboarding
37
+ offer to connect it and does live state list it as available to connect.
38
+ Missing, not exactly ``true``, or no file: not offered (standard installs).
39
+ - ``default_model``: optional, one entry of ``allowed_models`` that is the
40
+ install's default route (team profile: Claude Opus 5.5). Without it the
41
+ normal slot order applies (HyperSpeed V2 first).
42
+
43
+ No managed file: a team install's Hyper-signed team profile
44
+ (``hyperclaw.teamsetup.profile``, placed by ``hyperclaw onboard --team``) supplies
45
+ these fields. It is verified on every read; one that doesn't verify is
46
+ ignored and the defaults apply.
47
+
48
+ When the gateway catalog is live it will carry the allowed list; until then
49
+ this file is the source.
50
+ """
51
+ from __future__ import annotations
52
+
53
+ import json
54
+ import logging
55
+ import os
56
+ import re
57
+ import sys
58
+ from pathlib import Path
59
+ from typing import Optional
60
+
61
+ log = logging.getLogger("hyperclaw.managed_config")
62
+
63
+ ENV = "HYPERCLAW_MANAGED_CONFIG"
64
+ SCHEMA = 1
65
+ MAC_PATH = "/Library/Application Support/HyperClaw/managed.json"
66
+ UNIX_PATH = "/etc/hyperclaw/managed.json"
67
+
68
+ _ENTRY = re.compile(r"^[a-z0-9_]{1,40}/(\*|[A-Za-z0-9._:-][A-Za-z0-9._:/-]{0,127})$")
69
+ _PROVIDER = re.compile(r"^[a-z0-9_]{1,40}$")
70
+ _logged: set = set()
71
+
72
+
73
+ def path() -> Path:
74
+ explicit = os.environ.get(ENV, "").strip()
75
+ if explicit:
76
+ return Path(explicit).expanduser()
77
+ return Path(MAC_PATH if sys.platform == "darwin" else UNIX_PATH)
78
+
79
+
80
+ def _warn_once(msg: str) -> None:
81
+ if msg not in _logged:
82
+ _logged.add(msg)
83
+ log.warning(msg)
84
+
85
+
86
+ def load() -> dict:
87
+ """The validated managed config, or {} when there is none (or it is
88
+ unreadable; then the defaults apply, which are the strictest)."""
89
+ p = path()
90
+ try:
91
+ if not p.is_file():
92
+ return _team_profile()
93
+ data = json.loads(p.read_text(encoding="utf-8"))
94
+ except Exception as e: # noqa: BLE001 -- never fatal: defaults apply
95
+ _warn_once(f"managed config {p} unreadable ({type(e).__name__}); defaults apply")
96
+ return {}
97
+ if not isinstance(data, dict):
98
+ _warn_once(f"managed config {p} is not a JSON object; defaults apply")
99
+ return {}
100
+ out: dict = {}
101
+ allowed = data.get("allowed_models")
102
+ if isinstance(allowed, list):
103
+ good = [e.strip() for e in allowed if isinstance(e, str) and _ENTRY.match(e.strip())]
104
+ if len(good) != len(allowed):
105
+ _warn_once(f"managed config {p}: invalid allowed_models entries dropped")
106
+ out["allowed_models"] = list(dict.fromkeys(good))
107
+ issued = data.get("hyper_issued_keys")
108
+ if isinstance(issued, list):
109
+ out["hyper_issued_keys"] = sorted({x for x in issued
110
+ if isinstance(x, str) and _PROVIDER.match(x)})
111
+ if "telegram" in data:
112
+ if not isinstance(data["telegram"], bool):
113
+ _warn_once(f"managed config {p}: telegram must be true or false; not offered")
114
+ out["telegram"] = data["telegram"] is True
115
+ default = data.get("default_model")
116
+ if isinstance(default, str) and _ENTRY.match(default.strip()) \
117
+ and default.strip() in out.get("allowed_models", []):
118
+ out["default_model"] = default.strip()
119
+ names = data.get("model_names")
120
+ if isinstance(names, dict):
121
+ out["model_names"] = {k: re.sub(r"[\x00-\x1f\x7f]", "", v)[:60].strip()
122
+ for k, v in names.items()
123
+ if isinstance(k, str) and _ENTRY.match(k)
124
+ and isinstance(v, str) and v.strip()}
125
+ return out
126
+
127
+
128
+ def raw() -> tuple:
129
+ """(data, readable) for readers of keys this module doesn't validate
130
+ (``restricted_clients``, ``restricted_clients_required``,
131
+ ``google_calendar_write``): the system managed file when present, else
132
+ the verified team profile. (None, True): neither exists. (None, False):
133
+ a file exists but can't be read or doesn't verify (callers fail safe)."""
134
+ p = path()
135
+ try:
136
+ if p.is_file():
137
+ data = json.loads(p.read_text(encoding="utf-8"))
138
+ return (data, True) if isinstance(data, dict) else (None, False)
139
+ except Exception:
140
+ return None, False
141
+ try:
142
+ from hyperclaw.teamsetup import profile
143
+ if not profile.path().exists():
144
+ return None, True
145
+ view = profile.managed_view()
146
+ return (view, True) if view else (None, False)
147
+ except Exception:
148
+ return None, False
149
+
150
+
151
+ def source_key() -> str:
152
+ """Changes whenever the managed settings in force may have changed (for caches)."""
153
+ parts = []
154
+ for p in (path(), _team_profile_path()):
155
+ try:
156
+ parts.append(f"{p}:{p.stat().st_mtime_ns}")
157
+ except (OSError, TypeError):
158
+ parts.append(f"{p}:-")
159
+ return "|".join(parts)
160
+
161
+
162
+ def _team_profile_path():
163
+ try:
164
+ from hyperclaw.teamsetup import profile
165
+ return profile.path()
166
+ except Exception:
167
+ return None
168
+
169
+
170
+ def _team_profile() -> dict:
171
+ """The verified team profile's fields, or {} (hyperclaw.teamsetup.profile)."""
172
+ try:
173
+ from hyperclaw.teamsetup.profile import managed_view
174
+ return managed_view()
175
+ except Exception as e: # noqa: BLE001 -- never fatal: defaults apply
176
+ _warn_once(f"team profile unreadable ({type(e).__name__}); defaults apply")
177
+ return {}
178
+
179
+
180
+ # ───────────────────────── allowed-model list ─────────────────────────
181
+
182
+ def default_allowed() -> list:
183
+ """Hyper-deployed default: HyperSpeed V2 only."""
184
+ from hyperclaw import hyperspeed
185
+ return [f"{hyperspeed.PROVIDER_NAME}/{hyperspeed.model()}"]
186
+
187
+
188
+ def allowed_models() -> Optional[list]:
189
+ """The allowed list in force, or None when the install is not restricted
190
+ (standard installs)."""
191
+ from hyperclaw.deployment import is_hyper
192
+ if not is_hyper():
193
+ return None
194
+ return load().get("allowed_models") or default_allowed()
195
+
196
+
197
+ def is_allowed(provider: str, model: str) -> bool:
198
+ allowed = allowed_models()
199
+ if allowed is None:
200
+ return True
201
+ return f"{provider}/{model}" in allowed or f"{provider}/*" in allowed
202
+
203
+
204
+ def provider_allowed(provider: str) -> bool:
205
+ """True when any model of this provider is on the list (or no list)."""
206
+ allowed = allowed_models()
207
+ if allowed is None:
208
+ return True
209
+ return any(e.split("/", 1)[0] == provider for e in allowed)
210
+
211
+
212
+ def default_model() -> str:
213
+ """'<provider>/<model id>' the install routes to by default, or ''.
214
+ Only on a Hyper-deployed install, and only when it is on the list."""
215
+ from hyperclaw.deployment import is_hyper
216
+ if not is_hyper():
217
+ return ""
218
+ d = load().get("default_model") or ""
219
+ return d if d and "*" not in d and is_allowed(*d.split("/", 1)) else ""
220
+
221
+
222
+ def allowed_for(provider: str) -> list:
223
+ """Explicit model ids on the list for this provider ('*' included as-is)."""
224
+ allowed = allowed_models() or []
225
+ return [e.split("/", 1)[1] for e in allowed if e.split("/", 1)[0] == provider]
226
+
227
+
228
+ # ───────────────────────── key origin + names ─────────────────────────
229
+
230
+ def hyper_issued(provider: str) -> bool:
231
+ """True when Hyper issued this provider's key on this install."""
232
+ return provider in (load().get("hyper_issued_keys") or [])
233
+
234
+
235
+ def model_name(provider: str, model: str) -> str:
236
+ return (load().get("model_names") or {}).get(f"{provider}/{model}", "")
237
+
238
+
239
+ # ───────────────────────── install profile ─────────────────────────
240
+
241
+ def telegram_allowed() -> bool:
242
+ """True only when the managed config says the Telegram channel can run on
243
+ this install (``"telegram": true``; team and client profiles). Standard
244
+ installs without it are never offered Telegram in onboarding."""
245
+ return load().get("telegram") is True
hyperclaw/media_hub.py ADDED
@@ -0,0 +1,179 @@
1
+ """
2
+ Media Hub — unified outbound file delivery across every channel.
3
+
4
+ deliver_file(path, via, to, caption) is the single entry point:
5
+ via='here' -> queue for the current conversation (Telegram or iMessage,
6
+ whichever this turn came from) via outbox
7
+ via='telegram' -> Telegram sendPhoto/sendVideo/sendAudio/sendDocument
8
+ via='imessage' -> Messages.app attachment via AppleScript
9
+ via='email' -> Gmail with attachment (integrations_layer.gmail_send)
10
+ via='open' -> open the file locally on the Mac (Preview/Word/etc.)
11
+
12
+ Every sender is direct and synchronous — no daemon dependencies — so this works
13
+ from the TUI, the Telegram bot, the iMessage daemon, and cron jobs alike.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import subprocess
20
+ import tempfile
21
+ from pathlib import Path
22
+
23
+ import httpx
24
+
25
+ from . import outbox
26
+
27
+ MAX_TG_BYTES = 49 * 1024 * 1024
28
+
29
+
30
+ def _resolve(path: str) -> Path:
31
+ p = Path(path).expanduser()
32
+ if not p.exists():
33
+ raise FileNotFoundError(f"File not found: {p}")
34
+ return p
35
+
36
+
37
+ # ── Telegram ─────────────────────────────────────────────────────────────────
38
+
39
+ def telegram_send_file(path: str, caption: str = "", chat_id: str = "") -> str:
40
+ """Send any file over Telegram, picking the right API method by type."""
41
+ try:
42
+ p = _resolve(path)
43
+ _size = p.stat().st_size
44
+ except OSError as e:
45
+ return str(e)
46
+ token = os.environ.get("TELEGRAM_BOT_TOKEN", "")
47
+ cid = str(chat_id or os.environ.get("TELEGRAM_CHAT_ID", ""))
48
+ if not token or not cid:
49
+ return "Telegram not configured (TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID)"
50
+ if _size > MAX_TG_BYTES:
51
+ return f"File too large for Telegram ({_size // (1024*1024)}MB > 49MB). Email it instead."
52
+
53
+ kind = outbox.kind_of(str(p))
54
+ method, field = {
55
+ "photo": ("sendPhoto", "photo"),
56
+ "video": ("sendVideo", "video"),
57
+ "audio": ("sendAudio", "audio"),
58
+ "document": ("sendDocument", "document"),
59
+ }[kind]
60
+ data = {"chat_id": cid}
61
+ if caption:
62
+ data["caption"] = caption[:1024]
63
+ from hyperclaw.tools.receipts import redact, telegram_receipt
64
+ try:
65
+ with open(p, "rb") as f:
66
+ with httpx.Client(timeout=120) as client:
67
+ r = client.post(f"https://api.telegram.org/bot{token}/{method}",
68
+ data=data, files={field: (p.name, f)})
69
+ # Success only with Telegram's receipt: ok=true AND message_id (bench 20.3).
70
+ mid, why = telegram_receipt(r)
71
+ if mid:
72
+ return f"Sent {p.name} via Telegram ({kind}) [id:{mid}]"
73
+ # Photos with odd dimensions/format can fail — retry as document.
74
+ if kind == "photo":
75
+ with open(p, "rb") as f:
76
+ with httpx.Client(timeout=120) as client:
77
+ r2 = client.post(f"https://api.telegram.org/bot{token}/sendDocument",
78
+ data=data, files={"document": (p.name, f)})
79
+ mid, why = telegram_receipt(r2)
80
+ if mid:
81
+ return f"Sent {p.name} via Telegram (document) [id:{mid}]"
82
+ return f"Telegram send failed: {why}"
83
+ except Exception as e:
84
+ return f"Telegram send error: {redact(e)}"
85
+
86
+
87
+ # ── iMessage ─────────────────────────────────────────────────────────────────
88
+
89
+ def imessage_send_file(path: str, recipient: str = "", message: str = "") -> str:
90
+ """Send a file (and optional text) over iMessage via Messages.app."""
91
+ try:
92
+ p = _resolve(path)
93
+ except OSError as e:
94
+ return str(e)
95
+ recipient = recipient or os.environ.get("OWNER_PHONE", "") or os.environ.get("DEFAULT_PHONE", "")
96
+ if not recipient:
97
+ return "No iMessage recipient (set OWNER_PHONE)"
98
+ r_esc = recipient.replace("\\", "\\\\").replace('"', '\\"')
99
+ m_esc = (message or "").replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ").replace("\r", " ")
100
+ f_esc = str(p).replace("\\", "\\\\").replace('"', '\\"')
101
+ text_line = f'send "{m_esc}" to targetBuddy' if message else ""
102
+ script = f'''
103
+ tell application "Messages"
104
+ set targetService to 1st service whose service type = iMessage
105
+ set targetBuddy to buddy "{r_esc}" of targetService
106
+ {text_line}
107
+ send POSIX file "{f_esc}" to targetBuddy
108
+ end tell
109
+ '''
110
+ try:
111
+ res = subprocess.run(["osascript", "-e", script], capture_output=True, text=True, timeout=60)
112
+ if res.returncode == 0:
113
+ return f"Sent {p.name} via iMessage to {recipient}"
114
+ return f"iMessage send failed: {res.stderr.strip()[:200]}"
115
+ except Exception as e:
116
+ return f"iMessage send error: {e}"
117
+
118
+
119
+ # ── Email ────────────────────────────────────────────────────────────────────
120
+
121
+ def email_send_file(path: str, to: str, subject: str = "", body: str = "", cc: str = "") -> str:
122
+ """Email a file as an attachment via Gmail."""
123
+ try:
124
+ p = _resolve(path)
125
+ except OSError as e:
126
+ return str(e)
127
+ try:
128
+ from .integrations_layer import gmail_send
129
+ result = gmail_send(
130
+ to=to,
131
+ subject=subject or f"File: {p.name}",
132
+ body=body or f"Attached: {p.name}",
133
+ cc=cc,
134
+ attachments=[str(p)],
135
+ )
136
+ if isinstance(result, dict) and result.get("status") == "sent" and result.get("id"):
137
+ return f"Emailed {p.name} to {to} [id:{result['id']}]"
138
+ if isinstance(result, dict) and result.get("status") == "sent":
139
+ return "Email send failed: Gmail returned no message id, so delivery is not confirmed"
140
+ return f"Email send failed: {result}"
141
+ except Exception as e:
142
+ return f"Email send error: {e}"
143
+
144
+
145
+ # ── Local ────────────────────────────────────────────────────────────────────
146
+
147
+ def open_file(path: str, app: str = "") -> str:
148
+ """Open a file locally on the Mac (default app, or a named app)."""
149
+ try:
150
+ p = _resolve(path)
151
+ cmd = ["open", str(p)] if not app else ["open", "-a", app, str(p)]
152
+ res = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
153
+ if res.returncode == 0:
154
+ return f"Opened {p.name}" + (f" in {app}" if app else "")
155
+ return f"Open failed: {res.stderr.strip()[:200]}"
156
+ except subprocess.TimeoutExpired:
157
+ return "Open failed: timed out after 30s"
158
+ except OSError as e:
159
+ return f"Open failed: {e}"
160
+
161
+
162
+ # ── Unified entry ────────────────────────────────────────────────────────────
163
+
164
+ def deliver_file(path: str, via: str = "here", to: str = "", caption: str = "", app: str = "") -> str:
165
+ """Single entry point for delivering a file anywhere."""
166
+ via = (via or "here").lower().strip()
167
+ if via in ("here", "chat", "conversation"):
168
+ return outbox.queue_file(path, caption=caption)
169
+ if via == "telegram":
170
+ return telegram_send_file(path, caption=caption, chat_id=to)
171
+ if via == "imessage":
172
+ return imessage_send_file(path, recipient=to, message=caption)
173
+ if via == "email":
174
+ if not to:
175
+ return "Email delivery needs a recipient (to=...)"
176
+ return email_send_file(path, to=to, body=caption)
177
+ if via == "open":
178
+ return open_file(path, app=app or to)
179
+ return f"Unknown delivery channel: {via} (use here|telegram|imessage|email|open)"