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,318 @@
1
+ """
2
+ The tool registry (tool standard S1) and its dispatcher.
3
+
4
+ One entry per tool: name, description, args model (Pydantic, extra=forbid;
5
+ the advertised schema is generated from it), effect, scope, tier,
6
+ retry_budget and idempotent_on. The dispatcher is a dictionary lookup by
7
+ name and knows nothing about individual tools. Every outcome — unknown
8
+ tool, malformed or invalid arguments, a tool not offered on this surface,
9
+ a handler exception, a timeout, or the tool's own result — comes back as a
10
+ ToolResult envelope (S2); dispatch never raises.
11
+
12
+ Broker mapping (hyper-nimbus-gtm agent-mode design, READ/PROPOSE/ACT):
13
+ effect "read" is READ; every other effect is PROPOSE (in the approval wave an
14
+ `ask` tool returns needs_approval with a proposal instead of running); ACT is
15
+ never a tool here — execution after approval is a runtime transition. See
16
+ docs/TOOL-STANDARD.md.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import contextvars
21
+ import logging
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
24
+
25
+ from hyperclaw.chat_scope import OWNER, normalize as normalize_scope
26
+
27
+ from . import args as targs
28
+ from .classify import classify
29
+ from .envelope import (REASON_SURFACE, REASON_UNKNOWN_TOOL, Status, ToolResult, make)
30
+ from .surfaces import NEVER_FROM_CHAT, allowed_on_surface
31
+
32
+ log = logging.getLogger("hyperclaw.tools")
33
+
34
+ EFFECTS = ("read", "write", "send", "spend", "delete", "system")
35
+ TIERS = ("auto", "ask", "never_from_chat", "never")
36
+ PUBLIC = "public" # tool scope a guest chat may use; anything else is owner-only
37
+
38
+
39
+ # The turn guard of the tool call running right now (team_delegate reads the
40
+ # lead's provenance and requester from it). Set only around a dispatch.
41
+ _ACTIVE_GUARD: contextvars.ContextVar = contextvars.ContextVar("hyperclaw_active_guard",
42
+ default=None)
43
+
44
+
45
+ def active_guard():
46
+ return _ACTIVE_GUARD.get()
47
+
48
+
49
+ class ToolTimeout(Exception):
50
+ """Raised by a `run` hook when a tool exceeded its time budget."""
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class ToolSpec:
55
+ name: str
56
+ description: str
57
+ args_model: type
58
+ effect: str
59
+ scope: str
60
+ tier: str
61
+ retry_budget: int
62
+ idempotent_on: Tuple[str, ...]
63
+ handler: Callable[[str, dict], Any] = field(compare=False, repr=False)
64
+ # Wave 2: the idempotent_on fields that name the TARGET (the rest are the
65
+ # payload). A single path field makes the target "file:<resolved path>".
66
+ target_on: Tuple[str, ...] = ()
67
+
68
+ def __post_init__(self):
69
+ if self.effect not in EFFECTS:
70
+ raise ValueError(f"{self.name}: bad effect {self.effect!r}")
71
+ if self.tier not in TIERS:
72
+ raise ValueError(f"{self.name}: bad tier {self.tier!r}")
73
+ if self.name in NEVER_FROM_CHAT and self.tier not in ("never_from_chat", "never"):
74
+ raise ValueError(f"{self.name} must be tier never_from_chat (or never)")
75
+
76
+ @property
77
+ def broker_class(self) -> str:
78
+ """READ / PROPOSE in the agent-mode broker's terms (ACT is a transition)."""
79
+ return "READ" if self.effect == "read" else "PROPOSE"
80
+
81
+ def input_schema(self) -> dict:
82
+ return targs.input_schema(self.args_model)
83
+
84
+ def anthropic(self) -> dict:
85
+ return {"name": self.name, "description": self.description,
86
+ "input_schema": self.input_schema()}
87
+
88
+ def openai(self) -> dict:
89
+ return {"type": "function", "function": {
90
+ "name": self.name, "description": self.description,
91
+ "parameters": self.input_schema()}}
92
+
93
+
94
+ def surface_allows(spec: ToolSpec, surface: str, scope: str = OWNER) -> bool:
95
+ """May `spec` be offered/run on `surface` for a chat of `scope`?"""
96
+ if spec.tier == "never": # refused everywhere: its effect cannot be shown up front
97
+ return False
98
+ if not allowed_on_surface(spec.name, surface):
99
+ return False
100
+ if spec.tier == "never_from_chat" and surface != "tui":
101
+ return False
102
+ if normalize_scope(scope) != OWNER and spec.scope != PUBLIC:
103
+ return False
104
+ return True
105
+
106
+
107
+ class ToolRegistry:
108
+ def __init__(self, specs: Iterable[ToolSpec] = ()):
109
+ self._specs: Dict[str, ToolSpec] = {}
110
+ for s in specs:
111
+ self.register(s)
112
+
113
+ def register(self, spec: ToolSpec) -> None:
114
+ if spec.name in self._specs:
115
+ raise ValueError(f"duplicate tool {spec.name}")
116
+ self._specs[spec.name] = spec
117
+
118
+ def get(self, name: str) -> Optional[ToolSpec]:
119
+ return self._specs.get(name) if isinstance(name, str) else None
120
+
121
+ def names(self) -> List[str]:
122
+ return list(self._specs)
123
+
124
+ def __len__(self) -> int:
125
+ return len(self._specs)
126
+
127
+ def __contains__(self, name) -> bool:
128
+ return name in self._specs
129
+
130
+ def specs(self, surface: str = "tui", scope: str = OWNER) -> List[ToolSpec]:
131
+ return [s for s in self._specs.values() if surface_allows(s, surface, scope)]
132
+
133
+ def anthropic_tools(self, surface: str = "tui", scope: str = OWNER) -> List[dict]:
134
+ return [s.anthropic() for s in self.specs(surface, scope)]
135
+
136
+ def openai_tools(self, surface: str = "tui", scope: str = OWNER) -> List[dict]:
137
+ return [s.openai() for s in self.specs(surface, scope)]
138
+
139
+ def dispatch(self, name: str, raw_args: Any, *, surface: str = "tui",
140
+ scope: str = OWNER,
141
+ run: Optional[Callable[[ToolSpec, dict], Any]] = None,
142
+ guard: Any = None, approved_hash: Optional[str] = None) -> ToolResult:
143
+ """Look `name` up and run it. Never raises; always an envelope.
144
+
145
+ raw_args: a dict, or the raw JSON string from an OpenAI-style call
146
+ (malformed JSON -> invalid_args, never {}).
147
+ run(spec, args): optional executor hook (e.g. a timeout wrapper);
148
+ raise ToolTimeout from it on a timeout.
149
+ guard: the turn's tools.guard.TurnGuard (wave 2: effect ledger,
150
+ retry budgets, turn cap). Every tool loop passes one; None only in
151
+ direct calls (tests, internal use).
152
+ approved_hash: set only by tools.approval.send() when the owner pressed
153
+ Send; the call runs only if its payload hash is exactly that one.
154
+ Never reachable from a model's tool call.
155
+
156
+ Taint guard (hyperclaw.taint): the call runs as part of the guard's
157
+ conversation, so connector reads are recorded in its ledger; a
158
+ memory/learning tool whose arguments carry connector content (or, for
159
+ learning, whose conversation is tainted) is refused before any
160
+ approval card, with the reason.
161
+
162
+ Wave 3 (approval layer): after validation, tools.approval.evaluate()
163
+ decides whether the call runs now, is queued for the owner (Send /
164
+ Edit / Cancel; returns needs_approval and runs nothing), or is
165
+ refused. Every tool that is not read-only is gated, on every surface
166
+ including the local terminal (bash, code, desktop control too). Model
167
+ tool loops always pass a guard; a call without one has no approval
168
+ channel, so a gated call is refused (nothing runs), on every surface.
169
+ """
170
+ from hyperclaw import taint
171
+ token = _ACTIVE_GUARD.set(guard)
172
+ try:
173
+ with taint.conversation(getattr(guard, "conversation", None)), \
174
+ taint.message(getattr(guard, "turn_id", None)):
175
+ return self._dispatch(name, raw_args, surface=surface, scope=scope, run=run,
176
+ guard=guard, approved_hash=approved_hash)
177
+ finally:
178
+ _ACTIVE_GUARD.reset(token)
179
+
180
+ def _dispatch(self, name: str, raw_args: Any, *, surface: str, scope: str,
181
+ run: Optional[Callable[[ToolSpec, dict], Any]], guard: Any,
182
+ approved_hash: Optional[str]) -> ToolResult:
183
+ spec = self.get(name)
184
+ if spec is None:
185
+ return make(Status.NOT_FOUND, f"Tool '{name}' does not exist.",
186
+ reason=REASON_UNKNOWN_TOOL)
187
+ if not surface_allows(spec, surface, scope):
188
+ return make(Status.PERMISSION_DENIED,
189
+ f"Tool '{name}' is not available in this conversation; it was NOT run.",
190
+ reason=REASON_SURFACE)
191
+ if guard is not None:
192
+ refused = guard.precheck(spec)
193
+ if refused is not None:
194
+ return refused
195
+ try:
196
+ args = targs.validate(spec.args_model, raw_args)
197
+ except targs.ArgsError as e:
198
+ if guard is not None:
199
+ guard.on_invalid(spec)
200
+ return make(Status.INVALID_ARGS, f"{name}: {e}")
201
+ from hyperclaw import taint
202
+ tainted = taint.check_tool_call(name, args, getattr(guard, "conversation", None))
203
+ if tainted is not None:
204
+ return make(Status.PERMISSION_DENIED, f"{name} was NOT run. {tainted.user_message}")
205
+ budgets = getattr(guard, "check_budgets", None) if approved_hash is None else None
206
+ if budgets is not None:
207
+ try:
208
+ refused = budgets(spec, args)
209
+ except Exception as e: # fail closed
210
+ log.warning("guard budget check failed for %s", name, exc_info=True)
211
+ return make(Status.FAILED, f"{name} was NOT run: runtime guard error "
212
+ f"({type(e).__name__}).")
213
+ if refused is not None:
214
+ return refused
215
+ gated = self._approval_gate(spec, args, raw_args, surface, scope, guard, approved_hash)
216
+ if gated is not None:
217
+ return gated
218
+ ctx = None
219
+ if guard is not None:
220
+ try:
221
+ refused, ctx = guard.before(spec, args)
222
+ except Exception as e: # fail closed: a guard bug never runs the tool unchecked
223
+ log.warning("guard failed for %s", name, exc_info=True)
224
+ return make(Status.FAILED, f"{name} was NOT run: runtime guard error "
225
+ f"({type(e).__name__}).")
226
+ if refused is not None:
227
+ return refused
228
+ env = self._execute(spec, args, run)
229
+ if guard is not None:
230
+ try:
231
+ env = guard.after(spec, args, env, ctx)
232
+ except Exception:
233
+ log.warning("guard.after failed for %s", name, exc_info=True)
234
+ return env
235
+
236
+ @staticmethod
237
+ def _approval_gate(spec: ToolSpec, args: dict, raw_args: Any, surface: str, scope: str,
238
+ guard: Any, approved_hash: Optional[str]) -> Optional[ToolResult]:
239
+ from . import approval
240
+ name = spec.name
241
+ if approved_hash is not None:
242
+ if not isinstance(raw_args, dict) or \
243
+ approval.payload_hash(name, raw_args) != approved_hash or \
244
+ approval.payload_hash(name, args) != approved_hash:
245
+ return make(Status.PERMISSION_DENIED,
246
+ f"{name} was NOT run: its arguments differ from the approved payload.")
247
+ return None
248
+ try:
249
+ gate = approval.evaluate(spec, args, surface=surface, guard=guard)
250
+ except Exception as e: # fail closed
251
+ log.warning("approval gate failed for %s", name, exc_info=True)
252
+ return make(Status.FAILED, f"{name} was NOT run: approval check error "
253
+ f"({type(e).__name__}).")
254
+ if gate is None:
255
+ return None
256
+ if gate.action == "deny":
257
+ env = make(Status.PERMISSION_DENIED, f"{name} was NOT run: {gate.reason}.")
258
+ _log_gate(guard, spec, args, env)
259
+ return env
260
+ if guard is None: # no approval channel: refused on every surface, terminal included
261
+ return make(Status.PERMISSION_DENIED,
262
+ f"{name} was NOT run: it needs the owner's approval ({gate.reason}) and "
263
+ f"this call has no approval channel.")
264
+ try:
265
+ item = approval.get_queue().propose(
266
+ tool=name, args=args, conversation=guard.conversation, surface=surface,
267
+ scope=normalize_scope(scope), requester=getattr(guard, "requester", "owner"),
268
+ kind=gate.kind, reason=gate.reason,
269
+ untrusted=bool(getattr(guard, "untrusted_seen", False)))
270
+ except Exception as e:
271
+ log.warning("approval queue unavailable for %s", name, exc_info=True)
272
+ return make(Status.FAILED, f"{name} was NOT run: the approval queue is unavailable "
273
+ f"({type(e).__name__}).")
274
+ env = approval.needs_approval_envelope(item)
275
+ guard.queued.append(item.id)
276
+ _log_gate(guard, spec, args, env)
277
+ return env
278
+
279
+ @staticmethod
280
+ def _execute(spec: ToolSpec, args: dict, run) -> ToolResult:
281
+ name = spec.name
282
+ try:
283
+ raw = run(spec, args) if run else spec.handler(name, args)
284
+ except ToolTimeout as e:
285
+ if spec.effect != "read":
286
+ return make(Status.UNCERTAIN,
287
+ f"{name} timed out ({e}); it may still have completed.")
288
+ return make(Status.FAILED, f"{name} timed out ({e}).", retryable=True)
289
+ except Exception as e: # a tool bug: report it, never retry the model call
290
+ from hyperclaw.taint import TaintedInput
291
+ if isinstance(e, TaintedInput): # a sink refused connector content: say why
292
+ return make(Status.PERMISSION_DENIED, f"{name} was NOT run. {e.user_message}")
293
+ log.warning("tool %s raised %s", name, type(e).__name__, exc_info=True)
294
+ return make(Status.FAILED, f"Error: tool {name} failed: {type(e).__name__}: {e}")
295
+ env = classify(spec, raw, args)
296
+ # Restricted-client guard, connector-result boundary: under an outside
297
+ # model, what involves a restricted client is withheld before the
298
+ # result can enter any model request (hyperclaw.client_guard).
299
+ from hyperclaw import client_guard
300
+ try:
301
+ return client_guard.filter_tool_result(name, env)
302
+ except Exception as e: # fail closed: nothing unchecked reaches the model
303
+ log.warning("client guard failed for %s", name, exc_info=True)
304
+ if name in client_guard.CONNECTOR_TOOL_SOURCES:
305
+ return make(Status.FAILED, f"{name}: the restricted-client check could not run "
306
+ f"({type(e).__name__}), so nothing it read is shown.")
307
+ return env
308
+
309
+
310
+ def _log_gate(guard, spec: ToolSpec, args: dict, env: ToolResult) -> None:
311
+ """Queued or refused calls go in the activity log too."""
312
+ if guard is None:
313
+ return
314
+ try:
315
+ from . import effects as fx
316
+ guard._log_refusal(spec, fx.call_effect(spec, args), args, env)
317
+ except Exception:
318
+ log.warning("could not log the approval decision for %s", spec.name, exc_info=True)
@@ -0,0 +1,151 @@
1
+ """
2
+ Service-control guard (benchmark 17.4 / 17.5).
3
+
4
+ Recovery and restart actions may only touch HyperClaw's own services, and
5
+ only with the owner's approval:
6
+
7
+ - A command that stops, starts, restarts, loads, unloads or kills a service or
8
+ process is a *service action*.
9
+ - Every label it names must match the allowlist (config-driven,
10
+ HYPERCLAW_SERVICE_ALLOWLIST, comma-separated globs; default
11
+ ``com.hyperclaw.*``). Anything else — another app's messaging service, a
12
+ bare pid, ``pkill``/``killall`` patterns — is refused outright.
13
+ - An allowlisted action still goes to the approval queue (never auto).
14
+ - Text the model read from email, the web or a document is data: a service
15
+ action asked for after such a read is refused unless the user named that
16
+ service in their own words (tools/approval.py applies this rule).
17
+
18
+ This is a guard for the model's tool calls (bash, python_exec, applescript),
19
+ not a sandbox.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import fnmatch
24
+ import os
25
+ import re
26
+ import shlex
27
+ from dataclasses import dataclass, field
28
+ from typing import List, Optional
29
+
30
+ DEFAULT_ALLOWLIST = ("com.hyperclaw.*",)
31
+
32
+ # Tools whose text argument can carry a service command.
33
+ COMMAND_ARGS = {"bash": "command", "python_exec": "code", "applescript": "script"}
34
+
35
+ _LAUNCHCTL_VERBS = {"kickstart", "bootout", "bootstrap", "load", "unload", "stop", "start",
36
+ "kill", "enable", "disable", "remove", "submit", "restart"}
37
+ _SYSTEMCTL_VERBS = {"restart", "stop", "start", "kill", "reload", "disable", "enable", "mask",
38
+ "unmask", "try-restart", "reload-or-restart"}
39
+ _KILLERS = {"kill", "pkill", "killall", "skill", "xkill"}
40
+ _SPLIT = re.compile(r"\|\||&&|[;|&\n]|\$\(|`")
41
+
42
+
43
+ def allowlist() -> tuple:
44
+ raw = os.environ.get("HYPERCLAW_SERVICE_ALLOWLIST", "")
45
+ items = tuple(x.strip() for x in raw.split(",") if x.strip())
46
+ return items or DEFAULT_ALLOWLIST
47
+
48
+
49
+ def label_allowed(label: str) -> bool:
50
+ return any(fnmatch.fnmatchcase(label, pat) for pat in allowlist())
51
+
52
+
53
+ @dataclass
54
+ class ServiceAction:
55
+ verdict: str # "deny" | "approve"
56
+ labels: List[str] = field(default_factory=list)
57
+ reason: str = ""
58
+
59
+
60
+ def _label_from_target(tok: str) -> str:
61
+ """gui/501/com.x.y -> com.x.y; ~/Library/LaunchAgents/com.x.y.plist -> com.x.y."""
62
+ t = tok.rstrip("/")
63
+ t = t.rsplit("/", 1)[-1]
64
+ if t.endswith(".plist"):
65
+ t = t[:-6]
66
+ if t.endswith(".service"):
67
+ t = t[:-8]
68
+ return t
69
+
70
+
71
+ def _segment_action(words: List[str]) -> Optional[ServiceAction]:
72
+ # drop leading sudo / env assignments
73
+ while words and (words[0] in ("sudo", "doas", "exec", "command", "nohup")
74
+ or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*=.*", words[0])):
75
+ words = words[1:]
76
+ if not words:
77
+ return None
78
+ prog = words[0].rsplit("/", 1)[-1]
79
+ if prog in _KILLERS:
80
+ return ServiceAction("deny", [], f"'{prog}' targets processes by pid or name, which "
81
+ f"cannot be limited to HyperClaw's own services")
82
+ if prog == "launchctl" and len(words) > 1 and words[1] in _LAUNCHCTL_VERBS:
83
+ targets = [w for w in words[2:] if not w.startswith("-") and not w.isdigit()]
84
+ labels = [_label_from_target(t) for t in targets if t not in ("system", "gui", "user")]
85
+ labels = [x for x in labels if x and not re.fullmatch(r"(gui|user|system)", x)]
86
+ return _decide(labels, f"launchctl {words[1]}")
87
+ if prog == "systemctl" and len(words) > 1:
88
+ rest = [w for w in words[1:] if not w.startswith("-")]
89
+ if rest and rest[0] in _SYSTEMCTL_VERBS:
90
+ return _decide([_label_from_target(w) for w in rest[1:]], f"systemctl {rest[0]}")
91
+ if prog == "service" and len(words) > 2:
92
+ return _decide([words[1]], f"service {words[2]}")
93
+ if prog == "brew" and len(words) > 2 and words[1] == "services" and words[2] in (
94
+ "start", "stop", "restart", "kill", "run", "cleanup"):
95
+ return _decide(words[3:4] or ["(all)"], f"brew services {words[2]}")
96
+ return None
97
+
98
+
99
+ def _decide(labels: List[str], what: str) -> ServiceAction:
100
+ if not labels:
101
+ return ServiceAction("deny", [], f"{what} names no service label")
102
+ bad = [x for x in labels if not label_allowed(x)]
103
+ if bad:
104
+ return ServiceAction("deny", labels, f"{what} on {', '.join(bad)}: only HyperClaw's own "
105
+ f"services ({', '.join(allowlist())}) may be touched")
106
+ return ServiceAction("approve", labels, f"{what} on {', '.join(labels)}")
107
+
108
+
109
+ def check_command(text: str, shell: bool = True) -> Optional[ServiceAction]:
110
+ """None when `text` contains no service action; else the verdict (any
111
+ denied segment denies the whole command)."""
112
+ if not isinstance(text, str) or not text.strip():
113
+ return None
114
+ found: List[ServiceAction] = []
115
+ for seg in _SPLIT.split(text):
116
+ seg = seg.strip()
117
+ if not seg:
118
+ continue
119
+ try:
120
+ words = shlex.split(seg, comments=True)
121
+ except ValueError:
122
+ words = seg.split()
123
+ act = _segment_action(words)
124
+ if act:
125
+ found.append(act)
126
+ # Code in other languages (python_exec subprocess lists, AppleScript
127
+ # "do shell script"): look for the programs as plain words too.
128
+ words_re = r"\b(launchctl|systemctl)\b" if shell else r"\b(launchctl|systemctl|pkill|killall|kill)\b"
129
+ if not found and re.search(words_re, text):
130
+ m = re.search(r"\b(launchctl|systemctl)\b[\s'\",\[]+(\w[\w-]*)", text)
131
+ labels = re.findall(r"(?:gui|user|system)/\d*/?([\w.\-]+)|([\w\-]+(?:\.[\w\-]+){2,})", text)
132
+ names = [a or b for a, b in labels]
133
+ if m and m.group(2) in (_LAUNCHCTL_VERBS | _SYSTEMCTL_VERBS) and names:
134
+ found.append(_decide(names, f"{m.group(1)} {m.group(2)}"))
135
+ else:
136
+ found.append(ServiceAction("deny", [], "process/service control from code cannot be "
137
+ "limited to HyperClaw's own services"))
138
+ if not found:
139
+ return None
140
+ denied = [a for a in found if a.verdict == "deny"]
141
+ if denied:
142
+ return denied[0]
143
+ labels = [x for a in found for x in a.labels]
144
+ return ServiceAction("approve", labels, "; ".join(a.reason for a in found))
145
+
146
+
147
+ def check_tool(name: str, args: dict) -> Optional[ServiceAction]:
148
+ field_name = COMMAND_ARGS.get(name)
149
+ if not field_name or not isinstance(args, dict):
150
+ return None
151
+ return check_command(args.get(field_name, ""), shell=(name == "bash"))
@@ -0,0 +1,54 @@
1
+ """
2
+ Which tools a surface may offer.
3
+
4
+ Spec cross-cutting rule 4 / §6: "shell and code execution from remote
5
+ channels, and desktop/app control" are tier never_from_chat. They are
6
+ REMOVED from every chat/remote surface (absent from the tool list and
7
+ refused if called anyway), not gated behind a confirmation.
8
+
9
+ Only the local terminal (surface "tui") keeps them, and there every call
10
+ that is not read-only goes through the approval layer (tools/approval.py):
11
+ the owner sees the exact command line, script, keystrokes, click, app, file
12
+ or address and answers Send / Edit / Cancel before anything runs.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ # Shell / code execution and desktop or device control.
17
+ NEVER_FROM_CHAT = frozenset({
18
+ "bash", # shell
19
+ "python_exec", # bare exec() of model-written code
20
+ "applescript", # arbitrary macOS automation
21
+ "type_text", # keyboard
22
+ "key_press", # keyboard
23
+ "mouse_click", # mouse
24
+ "open_app", # app control
25
+ "open_file", # opens files/apps on the owner's desktop
26
+ "open_url", # drives the owner's browser
27
+ "screenshot", # captures the owner's screen
28
+ "record_audio", # records the owner's microphone
29
+ })
30
+
31
+ # Tools only the lead (the person's own agent) has: never offered to or run by
32
+ # a specialist (surface "swarm"). Specialists can't start specialists.
33
+ LEAD_ONLY = frozenset({"team_delegate", "agent_start", "agent_stop", "agent_watch"})
34
+ SPECIALIST_SURFACE = "swarm"
35
+
36
+ LOCAL_SURFACES = frozenset({"tui"})
37
+ # Known remote surfaces. Any surface name not in LOCAL_SURFACES is treated as
38
+ # remote, including unknown ones (fail closed).
39
+ REMOTE_SURFACES = frozenset({"telegram", "imessage", "bridge", "server", "scheduler", "swarm"})
40
+
41
+
42
+ def is_local(surface: str) -> bool:
43
+ return surface in LOCAL_SURFACES
44
+
45
+
46
+ def allowed_on_surface(name: str, surface: str) -> bool:
47
+ if surface == SPECIALIST_SURFACE and name in LEAD_ONLY:
48
+ return False
49
+ return is_local(surface) or name not in NEVER_FROM_CHAT
50
+
51
+
52
+ def tools_for_surface(tools: list, surface: str) -> list:
53
+ """Filter an Anthropic-format tool list for a surface."""
54
+ return [t for t in tools if allowed_on_surface(t.get("name", ""), surface)]
@@ -0,0 +1,87 @@
1
+ """
2
+ Writes only to the workspace (benchmark 12.5, rc3).
3
+
4
+ Every tool that writes a file (write_file, edit_file, download, qr_code,
5
+ image_generate, sql_query) resolves the real target path first, following
6
+ symlinks and '..', and refuses anything outside the workspace,
7
+ ``$HYPERCLAW_ROOT/workspace`` (default ``~/.hyperclaw/workspace``). A
8
+ relative path is taken relative to the workspace, not the process cwd.
9
+
10
+ Refusals start with ``Permission denied:`` so the registry returns a
11
+ ``permission_denied`` envelope and the model is told not to work around it.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ # tool name -> the argument(s) naming a file the tool writes
20
+ WRITE_PATH_ARGS = {
21
+ "write_file": ("path",),
22
+ "edit_file": ("path",),
23
+ "download": ("path",),
24
+ "qr_code": ("save_path",),
25
+ "image_generate": ("save_path",),
26
+ "sql_query": ("database",),
27
+ "document_create": ("path",),
28
+ }
29
+
30
+ DENIED_PREFIX = "Permission denied:"
31
+
32
+
33
+ def workspace_root() -> Path:
34
+ """The real (symlink-resolved) workspace directory for this install."""
35
+ from hyperclaw.userconfig import root
36
+ ws = root() / "workspace"
37
+ return Path(os.path.realpath(ws))
38
+
39
+
40
+ def resolve(path, base: Optional[Path] = None) -> Path:
41
+ """The real path a write to `path` would land on: '~' expanded,
42
+ relative to the workspace, every symlink and '..' resolved (including a
43
+ symlinked final component)."""
44
+ base = base or workspace_root()
45
+ p = Path(os.path.expanduser(str(path)))
46
+ if not p.is_absolute():
47
+ p = base / p
48
+ return Path(os.path.realpath(p))
49
+
50
+
51
+ def inside_workspace(path, base: Optional[Path] = None) -> bool:
52
+ base = base or workspace_root()
53
+ real = resolve(path, base)
54
+ try:
55
+ real.relative_to(base)
56
+ return True
57
+ except ValueError:
58
+ return False
59
+
60
+
61
+ def check_write(tool: str, args: dict) -> Optional[str]:
62
+ """None if every path `tool` would write is inside the workspace;
63
+ otherwise the refusal text (nothing is written)."""
64
+ names = WRITE_PATH_ARGS.get(tool)
65
+ if not names or not isinstance(args, dict):
66
+ return None
67
+ base = workspace_root()
68
+ for key in names:
69
+ val = args.get(key)
70
+ if val in (None, ""):
71
+ continue
72
+ try:
73
+ ok = inside_workspace(val, base)
74
+ except (OSError, ValueError, RuntimeError):
75
+ ok = False
76
+ if not ok:
77
+ return (f"{DENIED_PREFIX} {tool} may only write inside the HyperClaw workspace "
78
+ f"({base}); {val!r} resolves outside it. Nothing was written.")
79
+ return None
80
+
81
+
82
+ def workspace_path(path) -> Path:
83
+ """Resolve `path` for writing, or raise PermissionError if outside."""
84
+ base = workspace_root()
85
+ if not inside_workspace(path, base):
86
+ raise PermissionError(f"{path!r} is outside the HyperClaw workspace ({base})")
87
+ return resolve(path, base)