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,210 @@
1
+ """
2
+ Read-only egress rules (Vaughn's gate C-364: "a tool marked read-only must
3
+ never send data").
4
+
5
+ GET/HEAD alone is not enough: data can leave in a query string or through a
6
+ link preview. So:
7
+
8
+ - A read-only fetch may only target a URL that appears VERBATIM in what the
9
+ user wrote or in content the user gave it (a pasted message or an attached
10
+ document, which arrive inside the user's own message). Any other URL — one
11
+ the model built, or one it found inside an email, web page or file it read
12
+ — goes to the approval queue.
13
+ - An auto-tier fetch never reaches a loopback, private, link-local or other
14
+ non-public address without the owner's approval.
15
+ - Every Telegram message goes out with link previews off and every Slack post
16
+ with unfurls off (the payload fragments below; see tests/unit/
17
+ test_approval_egress.py for the repo-wide check).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import ipaddress
22
+ import re
23
+ import socket
24
+ from typing import Iterable, List, Optional, Set
25
+ from urllib.parse import urlsplit, urlunsplit
26
+
27
+ # ───────────────────────── messaging payload fragments ─────────────────────────
28
+ # Telegram Bot API 7.0+: link_preview_options replaces disable_web_page_preview.
29
+ TELEGRAM_NO_PREVIEW = {"link_preview_options": {"is_disabled": True}}
30
+ # Slack chat.postMessage and incoming webhooks.
31
+ SLACK_NO_UNFURL = {"unfurl_links": False, "unfurl_media": False}
32
+
33
+
34
+ def telegram_payload(payload: dict) -> dict:
35
+ """`payload` with link previews off (a new dict)."""
36
+ out = dict(payload)
37
+ out.update(TELEGRAM_NO_PREVIEW)
38
+ return out
39
+
40
+
41
+ def slack_payload(payload: dict) -> dict:
42
+ """`payload` with link and media unfurls off (a new dict)."""
43
+ out = dict(payload)
44
+ out.update(SLACK_NO_UNFURL)
45
+ return out
46
+
47
+
48
+ # ───────────────────────── methods ─────────────────────────
49
+ READ_METHODS = frozenset({"GET", "HEAD"})
50
+
51
+ # Arguments of read-effect tools that name a URL the tool will fetch or hand
52
+ # to a service. Their values are checked against provenance.
53
+ URL_ARGS = frozenset({"url", "urls", "link", "href", "image_url", "src", "source_url"})
54
+
55
+ # ───────────────────────── URL provenance ─────────────────────────
56
+ _URL_RE = re.compile(r"""https?://[^\s<>"'`\])}]+""", re.IGNORECASE)
57
+ _BARE_RE = re.compile(r"""(?<![@\w.-])(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:/[^\s<>"'`\])}]*)?""",
58
+ re.IGNORECASE)
59
+ _TRAIL = ".,;:!?"
60
+
61
+
62
+ def _strip_trailing(u: str) -> str:
63
+ while u and u[-1] in _TRAIL:
64
+ u = u[:-1]
65
+ return u
66
+
67
+
68
+ def normalize_url(url: str) -> Optional[str]:
69
+ """Canonical form for comparison: lower-case scheme and host, default port
70
+ dropped, empty path -> '/', fragment dropped (it is never sent). The query
71
+ string is kept exactly: a different query is a different URL."""
72
+ try:
73
+ parts = urlsplit(str(url).strip())
74
+ except ValueError:
75
+ return None
76
+ scheme = (parts.scheme or "").lower()
77
+ if scheme not in ("http", "https") or not parts.hostname:
78
+ return None
79
+ host = parts.hostname.lower()
80
+ if ":" in host: # IPv6 literal
81
+ host = f"[{host}]"
82
+ try:
83
+ port = parts.port
84
+ except ValueError:
85
+ return None
86
+ if port and not ((scheme == "http" and port == 80) or (scheme == "https" and port == 443)):
87
+ host = f"{host}:{port}"
88
+ if parts.username or parts.password:
89
+ host = f"{parts.username or ''}{':' + parts.password if parts.password else ''}@{host}"
90
+ return urlunsplit((scheme, host, parts.path or "/", parts.query, ""))
91
+
92
+
93
+ def _schemeless(norm: str) -> str:
94
+ return norm.split("://", 1)[1]
95
+
96
+
97
+ def provenance_urls(texts: Iterable[str]) -> Set[str]:
98
+ """Every URL in the user's own words / user-given content, normalised.
99
+ A bare 'example.com/page' the user typed matches both http and https."""
100
+ found: Set[str] = set()
101
+ for t in texts or ():
102
+ if not isinstance(t, str) or not t:
103
+ continue
104
+ for m in _URL_RE.findall(t):
105
+ n = normalize_url(_strip_trailing(m))
106
+ if n:
107
+ found.add(n)
108
+ for m in _BARE_RE.findall(t):
109
+ m = _strip_trailing(m)
110
+ for scheme in ("https://", "http://"):
111
+ n = normalize_url(scheme + m)
112
+ if n:
113
+ found.add(n)
114
+ return found
115
+
116
+
117
+ def url_in_provenance(url: str, texts: Iterable[str]) -> bool:
118
+ """True when `url` (normalised) appears verbatim in `texts`."""
119
+ n = normalize_url(url)
120
+ if not n:
121
+ return False
122
+ return n in provenance_urls(texts)
123
+
124
+
125
+ # ───────────────────────── private / non-public targets ─────────────────────────
126
+ _PRIVATE_SUFFIXES = (".localhost", ".local", ".internal", ".lan", ".home.arpa", ".intranet")
127
+
128
+
129
+ def _ip_not_public(ip: ipaddress._BaseAddress) -> bool:
130
+ if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
131
+ ip = ip.ipv4_mapped
132
+ return bool(ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved
133
+ or ip.is_multicast or ip.is_unspecified or not ip.is_global)
134
+
135
+
136
+ def resolve_ips(host: str) -> List[str]:
137
+ """The host's addresses (tests replace this; a lookup failure is [])."""
138
+ try:
139
+ return sorted({ai[4][0] for ai in socket.getaddrinfo(host, None)})
140
+ except (OSError, UnicodeError):
141
+ return []
142
+
143
+
144
+ def is_private_target(url: str, resolve: bool = True) -> bool:
145
+ """True when `url` points at loopback, a private or link-local range (the
146
+ cloud metadata address 169.254.169.254 included), or another non-public
147
+ address. Unparseable URLs count as private (fail closed)."""
148
+ try:
149
+ host = urlsplit(str(url).strip()).hostname
150
+ except ValueError:
151
+ return True
152
+ if not host:
153
+ return True
154
+ host = host.lower().rstrip(".")
155
+ if host == "localhost" or host.endswith(_PRIVATE_SUFFIXES) or "." not in host and ":" not in host:
156
+ # localhost, *.local, and single-label names (resolved on the LAN)
157
+ try:
158
+ return _ip_not_public(ipaddress.ip_address(host))
159
+ except ValueError:
160
+ return True
161
+ try:
162
+ return _ip_not_public(ipaddress.ip_address(host))
163
+ except ValueError:
164
+ pass
165
+ # Integer / hex / octal IPv4 spellings ("2130706433", "0x7f.1") are not
166
+ # hostnames a public site uses: treat them as private.
167
+ if re.fullmatch(r"[0-9x.]+", host):
168
+ return True
169
+ if not resolve:
170
+ return False
171
+ for ip in resolve_ips(host):
172
+ try:
173
+ if _ip_not_public(ipaddress.ip_address(ip.split("%", 1)[0])):
174
+ return True
175
+ except ValueError:
176
+ return True
177
+ return False
178
+
179
+
180
+ # ───────────────────────── display ─────────────────────────
181
+ _SECRET_HEADER = re.compile(r"(?i)(authorization|cookie|token|secret|api[-_]?key|password|"
182
+ r"passwd|session|signature|credential|private)")
183
+
184
+
185
+ def mask_headers(headers) -> dict:
186
+ """Headers for display: secret-looking values are masked (the approved
187
+ payload itself keeps the real values; its hash binds them)."""
188
+ if not isinstance(headers, dict):
189
+ return {}
190
+ out = {}
191
+ for k, v in headers.items():
192
+ if _SECRET_HEADER.search(str(k)):
193
+ s = str(v)
194
+ out[str(k)] = f"•••• (masked, {len(s)} chars)"
195
+ else:
196
+ out[str(k)] = v
197
+ return out
198
+
199
+
200
+ def urls_in_args(args: dict) -> List[str]:
201
+ """The URL values among a tool call's URL arguments."""
202
+ out: List[str] = []
203
+ for k, v in (args or {}).items():
204
+ if k not in URL_ARGS:
205
+ continue
206
+ vals = v if isinstance(v, (list, tuple)) else [v]
207
+ for x in vals:
208
+ if isinstance(x, str) and x.strip():
209
+ out.append(x.strip())
210
+ return out
@@ -0,0 +1,188 @@
1
+ """
2
+ The structured result envelope (tool standard S2).
3
+
4
+ Every tool result the model sees is exactly this object, serialised as
5
+ compact JSON in the tool-result content:
6
+
7
+ {"status": "not_found", "retryable": false,
8
+ "message": "File ~/notes/invoice-9912.txt does not exist.",
9
+ "instruction": "Does not exist. Do not retry or search for it. ...",
10
+ "data": null, "receipt_id": null}
11
+
12
+ `instruction` comes from a fixed table keyed by status (and, for a few
13
+ statuses, a fixed reason), so the model is always told what to do next and
14
+ never has to infer it from free text.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import uuid
20
+ from enum import Enum
21
+ from typing import Any, Optional
22
+
23
+ from pydantic import BaseModel, ConfigDict
24
+
25
+
26
+ class Status(str, Enum):
27
+ OK = "ok"
28
+ NOT_FOUND = "not_found"
29
+ PERMISSION_DENIED = "permission_denied"
30
+ FAILED = "failed"
31
+ INVALID_ARGS = "invalid_args"
32
+ NEEDS_APPROVAL = "needs_approval" # ledger hold (wave 2); approval queue (wave 3)
33
+ DUPLICATE = "duplicate" # effect ledger (wave 2)
34
+ UNCERTAIN = "uncertain"
35
+ BUDGET_EXHAUSTED = "budget_exhausted"
36
+
37
+
38
+ # Reasons that select a more specific fixed instruction for a status.
39
+ REASON_NOT_CONFIGURED = "not_configured"
40
+ REASON_SURFACE = "surface"
41
+ REASON_UNKNOWN_TOOL = "unknown_tool"
42
+ REASON_RETRYABLE = "retryable"
43
+ # Wave 2 (effect ledger, retry budgets).
44
+ REASON_CONFIRM = "confirm" # needs_approval: held by the ledger
45
+ REASON_RESOURCE_MISSING = "resource_missing" # budget_exhausted: resource was not_found
46
+ REASON_RETRY_BUDGET = "retry_budget" # budget_exhausted: retries used up
47
+ REASON_TURN_CAP = "turn_cap" # budget_exhausted: tool-call cap for the turn
48
+
49
+ INSTRUCTIONS = {
50
+ (Status.OK, None): "",
51
+ (Status.NOT_FOUND, None):
52
+ "Does not exist. Do not retry or search for it. Tell the user it was not found "
53
+ "and ask where it is.",
54
+ (Status.NOT_FOUND, REASON_UNKNOWN_TOOL):
55
+ "This tool does not exist. Do not call it again. Tell the user you cannot do this here.",
56
+ (Status.PERMISSION_DENIED, None):
57
+ "Access was denied. Do not retry or work around it with another tool. Tell the user "
58
+ "you could not do it and why.",
59
+ (Status.PERMISSION_DENIED, REASON_NOT_CONFIGURED):
60
+ "This is not set up on this installation. Do not retry. Tell the user it is not set "
61
+ "up and what needs to be connected.",
62
+ (Status.PERMISSION_DENIED, REASON_SURFACE):
63
+ "This tool is not available in this conversation and was NOT run. Do not retry or "
64
+ "use another tool to do the same thing. Tell the user you cannot do this here.",
65
+ (Status.FAILED, None):
66
+ "It failed and nothing was done. Do not retry. Tell the user it failed and give the "
67
+ "reason from message.",
68
+ (Status.FAILED, REASON_RETRYABLE):
69
+ "It failed with a temporary error. You may retry once; if it fails again, tell the "
70
+ "user it failed and why.",
71
+ (Status.INVALID_ARGS, None):
72
+ "The arguments were invalid and nothing ran. Fix the arguments named in message and "
73
+ "call the tool once more. If you do not have the information, ask the user.",
74
+ (Status.NEEDS_APPROVAL, None):
75
+ "Not done yet: it is waiting for the user's approval. Tell the user exactly what is "
76
+ "queued. Never say it was done.",
77
+ (Status.NEEDS_APPROVAL, REASON_CONFIRM):
78
+ "It was NOT done and nothing is queued. Tell the user what you were about to do and "
79
+ "why it was held (see message), and ask them. Do not call it again in this reply.",
80
+ (Status.DUPLICATE, None):
81
+ "This was already done earlier and was not repeated. Tell the user it was already "
82
+ "done. Do not call it again.",
83
+ (Status.UNCERTAIN, None):
84
+ "It may or may not have happened. Do not retry. Tell the user the outcome is "
85
+ "uncertain and offer to check.",
86
+ (Status.BUDGET_EXHAUSTED, None):
87
+ "The runtime refused another attempt; it was NOT run. Do not call it again. Tell the "
88
+ "user what you tried and what did not work.",
89
+ (Status.BUDGET_EXHAUSTED, REASON_RESOURCE_MISSING):
90
+ "Stop. Tell the user what you tried and that it was not found.",
91
+ (Status.BUDGET_EXHAUSTED, REASON_RETRY_BUDGET):
92
+ "It already failed and its retry budget is used up; it was NOT run. Do not call it "
93
+ "again. Tell the user what failed and why.",
94
+ (Status.BUDGET_EXHAUSTED, REASON_TURN_CAP):
95
+ "The tool-call limit for this message was reached; it was NOT run. Stop calling "
96
+ "tools. Tell the user what you did and what did not run.",
97
+ }
98
+
99
+ # Default retryable flag per status. FAILED is per tool (see make()).
100
+ RETRYABLE = {
101
+ Status.OK: False,
102
+ Status.NOT_FOUND: False,
103
+ Status.PERMISSION_DENIED: False,
104
+ Status.FAILED: False,
105
+ Status.INVALID_ARGS: True, # once, with corrected arguments
106
+ Status.NEEDS_APPROVAL: False,
107
+ Status.DUPLICATE: False,
108
+ Status.UNCERTAIN: False,
109
+ Status.BUDGET_EXHAUSTED: False,
110
+ }
111
+
112
+ ENVELOPE_KEYS = ("status", "retryable", "message", "instruction", "data", "receipt_id")
113
+
114
+
115
+ class ToolResult(BaseModel):
116
+ """The only thing a tool returns to the model."""
117
+ model_config = ConfigDict(extra="forbid", use_enum_values=True)
118
+
119
+ status: Status
120
+ retryable: bool
121
+ message: str
122
+ instruction: str
123
+ data: Any = None
124
+ receipt_id: Optional[str] = None
125
+
126
+ @property
127
+ def ok(self) -> bool:
128
+ return self.status == Status.OK.value
129
+
130
+ def to_dict(self) -> dict:
131
+ return {k: getattr(self, k) for k in ENVELOPE_KEYS}
132
+
133
+ def to_content(self, limit: int = 10000) -> str:
134
+ """Compact JSON for the tool-result content, at most ~`limit` chars.
135
+ Over-long `data` is clipped (the JSON stays valid)."""
136
+ d = self.to_dict()
137
+ s = _dumps(d)
138
+ if len(s) <= limit:
139
+ return s
140
+ data = d["data"]
141
+ text = data if isinstance(data, str) else _dumps(data)
142
+ room = max(0, limit - (len(s) - len(_dumps(text))) - 60)
143
+ d["data"] = text[:room] + f"... [clipped: {len(text) - room} more characters]"
144
+ s = _dumps(d)
145
+ if len(s) > limit: # a huge message: clip it too
146
+ d["message"] = d["message"][:500]
147
+ d["data"] = (d["data"] or "")[: max(0, limit - 1000)]
148
+ s = _dumps(d)
149
+ return s
150
+
151
+
152
+ def _dumps(obj) -> str:
153
+ return json.dumps(obj, ensure_ascii=False, separators=(",", ":"), default=str)
154
+
155
+
156
+ def new_receipt_id() -> str:
157
+ return f"rcpt_{uuid.uuid4().hex[:16]}"
158
+
159
+
160
+ def make(status: Status, message: str = "", *, data: Any = None,
161
+ receipt_id: Optional[str] = None, reason: Optional[str] = None,
162
+ retryable: Optional[bool] = None) -> ToolResult:
163
+ status = Status(status)
164
+ if reason == REASON_RETRYABLE and status == Status.FAILED and retryable is None:
165
+ retryable = True
166
+ instruction = INSTRUCTIONS.get((status, reason)) or INSTRUCTIONS[(status, None)]
167
+ if retryable is None:
168
+ retryable = RETRYABLE[status]
169
+ if status == Status.FAILED and retryable:
170
+ instruction = INSTRUCTIONS[(Status.FAILED, REASON_RETRYABLE)]
171
+ return ToolResult(status=status, retryable=bool(retryable), message=message or "",
172
+ instruction=instruction, data=data, receipt_id=receipt_id)
173
+
174
+
175
+ def parse_content(content) -> Optional[dict]:
176
+ """The envelope dict if `content` is a serialised envelope, else None."""
177
+ if isinstance(content, list): # Anthropic content blocks
178
+ content = "".join(b.get("text", "") if isinstance(b, dict) else str(b) for b in content)
179
+ if not isinstance(content, str) or not content.startswith("{"):
180
+ return None
181
+ try:
182
+ d = json.loads(content)
183
+ except (ValueError, TypeError):
184
+ return None
185
+ if isinstance(d, dict) and set(ENVELOPE_KEYS) <= set(d) and d.get("status") in {
186
+ s.value for s in Status}:
187
+ return d
188
+ return None