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,717 @@
1
+ """
2
+ Restricted-client guard: content about a restricted client never reaches an
3
+ outside model.
4
+
5
+ Rule (Vaughn, 12 Oct 2026; CLIENT-AI-RESTRICTIONS 11 Oct): when the ACTIVE
6
+ model is from an outside provider (anything that is not Hyper's own model,
7
+ HyperSpeed V2), nothing involving a restricted client reaches it from Gmail,
8
+ Slack, Calendar, Meet transcripts, Drive, Docs, Sheets, Slides or GitLab,
9
+ whether the lead model or a specialist asked for it.
10
+
11
+ Two layers:
12
+
13
+ 1. **Connector-result boundary** (``screen`` / ``filter_tool_result`` /
14
+ ``filter_items``): before a connector read enters any model request, every
15
+ item (one email thread, one event, one file, one transcript, one Slack or
16
+ GitLab item) that names a restricted client (name, alias, domain or
17
+ project name, also inside forwards and threads that never show the
18
+ client's domain) is withheld. Anything that could not be checked (binary,
19
+ unreadable, an unknown shape) is withheld too: when unsure, withhold.
20
+ 2. **Final check on the outgoing request body** (``redact_request`` /
21
+ ``final_check``), run by the cost guard's adapters, which every model call
22
+ in the package already goes through (tests/unit/test_cost_guard.py). An
23
+ outside request that still carries restricted content has those strings
24
+ replaced by a plain note; whatever cannot be replaced stops the request
25
+ (``RestrictedContentBlocked``) and nothing is sent.
26
+
27
+ Every withholding is recorded on the current task, and the answer ends with
28
+ one plain line saying what was left out and that switching to HyperSpeed V2
29
+ includes it (``reply_note``). The note never names the client, so the note
30
+ itself can never trip the guard.
31
+
32
+ Configuration. No client is named in the package: it ships with an EMPTY
33
+ default list. The list comes from the install profile:
34
+
35
+ - the managed config (``hyperclaw/managed_config.py``), key
36
+ ``restricted_clients``: Hyper's list for the install (team and client
37
+ installs get it in their team profile);
38
+ - ``restricted_clients_required: true`` in the same file: the profile MUST
39
+ carry a list. If it is missing, empty or unreadable while an outside
40
+ model is active, ALL connector content is withheld, and the user is told
41
+ why (fail safe);
42
+ - ``$HYPERCLAW_ROOT/config/restricted_clients.json`` (or
43
+ ``HYPERCLAW_RESTRICTED_CLIENTS``): the owner may ADD entries, never
44
+ remove one.
45
+
46
+ With no list and nothing required, there is nothing to withhold.
47
+
48
+ Entry shape: ``{"name": "...", "aliases": [...], "domains": [...],
49
+ "projects": [...]}``.
50
+ """
51
+ from __future__ import annotations
52
+
53
+ import contextlib
54
+ import contextvars
55
+ import html
56
+ import json
57
+ import logging
58
+ import os
59
+ import re
60
+ import threading
61
+ import unicodedata
62
+ from dataclasses import dataclass, field
63
+ from pathlib import Path
64
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
65
+
66
+ log = logging.getLogger("hyperclaw.client_guard")
67
+
68
+ # Shipped empty on purpose: no client is named in shipped code. The real
69
+ # list arrives in each install's profile (managed config).
70
+ DEFAULT_CLIENTS: Tuple[dict, ...] = ()
71
+
72
+ # Said when the profile requires a list but has none (no client named).
73
+ PROFILE_MISSING_NOTE = ("Withheld: the restricted-client list is missing from your install "
74
+ "profile, so nothing from your connected accounts is shown to an "
75
+ "outside model. Ask Vaughn to fix your install profile, or switch to "
76
+ "HyperSpeed V2 (/model).")
77
+
78
+ USER_FILE = "restricted_clients.json"
79
+ ENV_FILE = "HYPERCLAW_RESTRICTED_CLIENTS"
80
+
81
+ # What the model and the user see in place of withheld content. It never
82
+ # names the client (so it can never trip the guard itself).
83
+ PLACEHOLDER = ("[withheld: this is about a client whose agreement keeps their data on Hyper's "
84
+ "own models; switch to HyperSpeed V2 to include it]")
85
+
86
+ # The sources this guard covers (taint origins and tool families).
87
+ SOURCES = ("gmail", "slack", "google_calendar", "google_meet", "google_drive", "google_docs",
88
+ "google_sheets", "google_slides", "gitlab")
89
+ SOURCE_LABELS = {
90
+ "gmail": "email", "slack": "Slack", "google_calendar": "calendar",
91
+ "google_meet": "Meet", "google_drive": "Drive", "google_docs": "Docs",
92
+ "google_sheets": "Sheets", "google_slides": "Slides", "gitlab": "GitLab",
93
+ "email": "email", "calendar": "calendar", "request": "earlier messages",
94
+ }
95
+
96
+ # Model tools whose results are connector content from the covered sources.
97
+ # The Google Workspace tools register theirs (hyperclaw/google_workspace).
98
+ CONNECTOR_TOOL_SOURCES: Dict[str, str] = {
99
+ "email_read": "gmail", "email_search": "gmail", "calendar_read": "google_calendar",
100
+ # GitLab reader (team of agents, rc5)
101
+ "gitlab_review_queue": "gitlab",
102
+ # hyperclaw/google_workspace/tools.py
103
+ "gmail_search": "gmail", "gmail_read_thread": "gmail", "gmail_read_attachment": "gmail",
104
+ "calendar_list_events": "google_calendar", "calendar_freebusy": "google_calendar",
105
+ "drive_search": "google_drive", "drive_read": "google_drive", "docs_read": "google_docs",
106
+ "sheets_read": "google_sheets", "slides_read": "google_slides",
107
+ "meet_list_records": "google_meet", "meet_read_transcript": "google_meet",
108
+ }
109
+ # One result is one item (a thread with all its messages, one file, one
110
+ # transcript): withheld whole, never message by message.
111
+ WHOLE_RESULT_TOOLS = frozenset({"gmail_read_thread", "gmail_read_attachment", "drive_read",
112
+ "docs_read", "sheets_read", "slides_read",
113
+ "meet_read_transcript"})
114
+ # Of those, the legacy readers that return headers only (no body): their
115
+ # results can't be checked for a forward or a thread that names the client
116
+ # in the body, so under an outside model they are withheld whole.
117
+ HEADER_ONLY_TOOLS = frozenset({"email_read", "email_search", "calendar_read"})
118
+
119
+
120
+ # ───────────────────────── configuration ─────────────────────────
121
+
122
+ @dataclass(frozen=True)
123
+ class RestrictedClient:
124
+ name: str
125
+ aliases: Tuple[str, ...] = ()
126
+ domains: Tuple[str, ...] = ()
127
+ projects: Tuple[str, ...] = ()
128
+
129
+ def terms(self) -> List[str]:
130
+ return [t for t in (self.name,) + self.aliases + self.projects if t]
131
+
132
+
133
+ def _strs(v: Any) -> Tuple[str, ...]:
134
+ if isinstance(v, str):
135
+ v = [v]
136
+ if not isinstance(v, (list, tuple)):
137
+ return ()
138
+ out = []
139
+ for x in v:
140
+ if isinstance(x, str) and x.strip():
141
+ out.append(x.strip())
142
+ return tuple(dict.fromkeys(out))
143
+
144
+
145
+ def _entry(d: Any) -> Optional[RestrictedClient]:
146
+ if not isinstance(d, dict):
147
+ return None
148
+ name = d.get("name")
149
+ if not isinstance(name, str) or not name.strip():
150
+ return None
151
+ domains = tuple(x.lower().lstrip("@").strip(".") for x in _strs(d.get("domains")))
152
+ return RestrictedClient(name=name.strip(), aliases=_strs(d.get("aliases")),
153
+ domains=tuple(x for x in domains if x),
154
+ projects=_strs(d.get("projects")))
155
+
156
+
157
+ def _root() -> Path:
158
+ return Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw")).expanduser()
159
+
160
+
161
+ def user_file() -> Path:
162
+ explicit = os.environ.get(ENV_FILE, "").strip()
163
+ if explicit:
164
+ return Path(explicit).expanduser()
165
+ return _root() / "config" / USER_FILE
166
+
167
+
168
+ def _read_list(p: Path) -> list:
169
+ try:
170
+ if not p.is_file():
171
+ return []
172
+ data = json.loads(p.read_text(encoding="utf-8"))
173
+ except Exception as e: # noqa: BLE001 -- never fatal: the defaults still apply
174
+ log.warning("restricted clients file %s unreadable (%s); defaults apply", p,
175
+ type(e).__name__)
176
+ return []
177
+ if isinstance(data, dict):
178
+ data = data.get("restricted_clients")
179
+ return data if isinstance(data, list) else []
180
+
181
+
182
+ def _managed() -> Tuple[list, bool, bool]:
183
+ """(profile list, list required, profile readable). A managed file that
184
+ exists but can't be read counts as requiring a list it doesn't have."""
185
+ try:
186
+ from hyperclaw import managed_config
187
+ data, ok = managed_config.raw() # system file, else the verified team profile
188
+ except Exception:
189
+ return [], True, False
190
+ if not ok:
191
+ return [], True, False
192
+ if data is None:
193
+ return [], False, True
194
+ lst = data.get("restricted_clients")
195
+ return ((lst if isinstance(lst, list) else []),
196
+ data.get("restricted_clients_required") is True, True)
197
+
198
+
199
+ def clients() -> List[RestrictedClient]:
200
+ """The restricted list in force: the install profile's list plus the
201
+ owner's additions (the package itself ships none)."""
202
+ defaults = [c for c in (_entry(d) for d in DEFAULT_CLIENTS) if c]
203
+ managed, _required, _ok = _managed()
204
+ mlist = [c for c in (_entry(d) for d in managed) if c]
205
+ extra = [c for c in (_entry(d) for d in _read_list(user_file())) if c]
206
+ return defaults + mlist + extra
207
+
208
+
209
+ def placeholder() -> str:
210
+ """What stands in for withheld content (never names a client)."""
211
+ return PROFILE_MISSING_NOTE if profile_missing() else PLACEHOLDER
212
+
213
+
214
+ def profile_missing() -> bool:
215
+ """The install profile requires a restricted-client list (or can't be
216
+ read) and has no valid entry: fail safe, withhold all connector content."""
217
+ managed, required, _ok = _managed()
218
+ if not required:
219
+ return False
220
+ return not any(_entry(d) for d in managed)
221
+
222
+
223
+ def active() -> bool:
224
+ """Is there anything to protect? A list in force, or a missing required one."""
225
+ return profile_missing() or bool(matcher()._pats)
226
+
227
+
228
+ # ───────────────────────── detection ─────────────────────────
229
+
230
+ _ZW = re.compile(r"[­​-‏⁠]")
231
+ _SEP = r"[\s\-_.·/]*"
232
+
233
+
234
+ def normalize(text: str) -> str:
235
+ """NFKC, HTML entities decoded, zero-width characters removed, lower case."""
236
+ t = unicodedata.normalize("NFKC", str(text))
237
+ t = html.unescape(t)
238
+ t = _ZW.sub("", t)
239
+ return t.lower()
240
+
241
+
242
+ def _term_pattern(term: str) -> str:
243
+ term = normalize(term).strip()
244
+ if re.fullmatch(r"[a-z]{2,6}", term) and term == term.strip():
245
+ # A short acronym: also "a.b.g." and "a b g".
246
+ body = r"[.\s]?".join(re.escape(ch) for ch in term) + r"\.?"
247
+ else:
248
+ parts = [p for p in re.split(r"[\s\-_.]+", term) if p]
249
+ body = _SEP.join(re.escape(p) for p in parts) if parts else re.escape(term)
250
+ return r"(?<![a-z0-9])" + body + r"(?![a-z0-9])"
251
+
252
+
253
+ def _domain_pattern(dom: str) -> str:
254
+ return r"(?<![a-z0-9-])" + re.escape(dom.lower()) + r"(?![a-z0-9-])"
255
+
256
+
257
+ class Matcher:
258
+ def __init__(self, entries: Iterable[RestrictedClient]):
259
+ self.entries = list(entries)
260
+ self._pats: List[Tuple[str, re.Pattern]] = []
261
+ for c in self.entries:
262
+ pats = [_term_pattern(t) for t in c.terms()] + [_domain_pattern(d)
263
+ for d in c.domains]
264
+ if pats:
265
+ self._pats.append((c.name, re.compile("|".join(pats))))
266
+
267
+ def find(self, text: Any) -> List[str]:
268
+ """Names of the restricted clients `text` involves (any string inside it)."""
269
+ found: List[str] = []
270
+ for s in strings(text):
271
+ n = normalize(s)
272
+ for name, pat in self._pats:
273
+ if name not in found and pat.search(n):
274
+ found.append(name)
275
+ return found
276
+
277
+ def hits(self, value: Any) -> bool:
278
+ return bool(self.find(value))
279
+
280
+
281
+ _CACHE: Dict[str, Any] = {}
282
+ _CACHE_LOCK = threading.Lock()
283
+
284
+
285
+ def matcher() -> Matcher:
286
+ """The matcher for the list in force (rebuilt when a config file changes)."""
287
+ key_parts = []
288
+ for p in (user_file(),):
289
+ try:
290
+ key_parts.append(f"{p}:{p.stat().st_mtime_ns}")
291
+ except OSError:
292
+ key_parts.append(f"{p}:-")
293
+ try:
294
+ from hyperclaw import managed_config
295
+ key_parts.append(managed_config.source_key())
296
+ except Exception:
297
+ key_parts.append("managed:-")
298
+ key = "|".join(key_parts)
299
+ with _CACHE_LOCK:
300
+ if _CACHE.get("key") != key:
301
+ _CACHE["key"] = key
302
+ _CACHE["m"] = Matcher(clients())
303
+ return _CACHE["m"]
304
+
305
+
306
+ def reset_cache() -> None:
307
+ with _CACHE_LOCK:
308
+ _CACHE.clear()
309
+
310
+
311
+ def strings(value: Any, depth: int = 0) -> Iterable[str]:
312
+ if depth > 12 or value is None:
313
+ return
314
+ if isinstance(value, str):
315
+ yield value
316
+ elif isinstance(value, dict):
317
+ for k, v in value.items():
318
+ if isinstance(k, str):
319
+ yield k
320
+ yield from strings(v, depth + 1)
321
+ elif isinstance(value, (list, tuple, set)):
322
+ for v in value:
323
+ yield from strings(v, depth + 1)
324
+ elif hasattr(value, "model_dump"):
325
+ yield from strings(value.model_dump(), depth + 1)
326
+ elif hasattr(value, "__dict__") and not isinstance(value, type):
327
+ yield from strings(vars(value), depth + 1)
328
+ elif isinstance(value, (int, float, bool)):
329
+ return
330
+ else:
331
+ yield str(value)
332
+
333
+
334
+ def checkable(value: Any, depth: int = 0) -> bool:
335
+ """False for anything this guard can't read as text (bytes, unknown objects)."""
336
+ if depth > 12:
337
+ return False
338
+ if value is None or isinstance(value, (str, int, float, bool)):
339
+ return True
340
+ if isinstance(value, (bytes, bytearray, memoryview)):
341
+ return False
342
+ if isinstance(value, dict):
343
+ return all(checkable(v, depth + 1) for v in value.values())
344
+ if isinstance(value, (list, tuple, set)):
345
+ return all(checkable(v, depth + 1) for v in value)
346
+ return False
347
+
348
+
349
+ # ───────────────────────── is the active model outside? ─────────────────────────
350
+
351
+ _OVERRIDE: contextvars.ContextVar = contextvars.ContextVar("hyperclaw_client_guard_model",
352
+ default=None)
353
+
354
+
355
+ def is_hyper_provider(provider: Optional[str]) -> bool:
356
+ """Hyper's own model (HyperSpeed V2, or a provider configured with origin
357
+ hyper). Anything unknown is outside."""
358
+ name = str(provider or "").strip()
359
+ if not name:
360
+ return False
361
+ try:
362
+ from hyperclaw import hyperspeed
363
+ if name == hyperspeed.PROVIDER_NAME:
364
+ return True
365
+ except Exception:
366
+ pass
367
+ try:
368
+ from hyperclaw.providers import registry
369
+ prov = registry().providers.get(name)
370
+ return prov is not None and getattr(prov, "origin", "") == "hyper"
371
+ except Exception:
372
+ return False
373
+
374
+
375
+ @contextlib.contextmanager
376
+ def model_scope(provider: str):
377
+ """Run the body as if `provider` were the active model (a caller that
378
+ knows exactly which model its reads go to: the digest, the brief, tests)."""
379
+ tok = _OVERRIDE.set(str(provider or ""))
380
+ try:
381
+ yield
382
+ finally:
383
+ _OVERRIDE.reset(tok)
384
+
385
+
386
+ def note_model(provider: str) -> None:
387
+ """Called by the cost guard before every model request: the providers a
388
+ task's model calls went to (lead and specialists share the task)."""
389
+ try:
390
+ from hyperclaw.cost_guard import current_task
391
+ t = current_task()
392
+ if t is not None:
393
+ t.model_providers.add(str(provider or "unknown"))
394
+ except Exception:
395
+ pass
396
+
397
+
398
+ def _third_party_live() -> bool:
399
+ """Could an outside model answer on this install at all? Unsure -> True."""
400
+ try:
401
+ from hyperclaw.providers import registry
402
+ for p in registry().providers.values():
403
+ if getattr(p, "origin", "customer") != "hyper" and getattr(p, "live", False):
404
+ return True
405
+ return False
406
+ except Exception:
407
+ return True
408
+
409
+
410
+ def outside_model_active() -> bool:
411
+ """True when content read now could reach an outside model.
412
+
413
+ 1. an explicit ``model_scope``;
414
+ 2. the current task's model calls so far: any outside provider -> True
415
+ (a specialist on an outside model counts);
416
+ 3. nothing known: True unless no outside provider is live on this
417
+ install. When unsure, withhold."""
418
+ o = _OVERRIDE.get()
419
+ if o is not None:
420
+ return not is_hyper_provider(o)
421
+ try:
422
+ from hyperclaw.cost_guard import current_task
423
+ t = current_task()
424
+ provs = set(getattr(t, "model_providers", ()) or ()) if t is not None else set()
425
+ except Exception:
426
+ provs = set()
427
+ if provs:
428
+ return any(not is_hyper_provider(p) for p in provs)
429
+ return _third_party_live()
430
+
431
+
432
+ # ───────────────────────── withheld notes ─────────────────────────
433
+
434
+ _FALLBACK_WITHHELD: Dict[str, int] = {}
435
+
436
+
437
+ def _record(source: str, n: int) -> None:
438
+ if n <= 0:
439
+ return
440
+ try:
441
+ from hyperclaw.cost_guard import current_task
442
+ t = current_task()
443
+ except Exception:
444
+ t = None
445
+ if t is None:
446
+ _FALLBACK_WITHHELD[source] = _FALLBACK_WITHHELD.get(source, 0) + n
447
+ return
448
+ t.withheld[source] = t.withheld.get(source, 0) + n
449
+
450
+
451
+ record = _record # public name for the connector readers
452
+
453
+
454
+ def withheld_counts(t: Any = None) -> Dict[str, int]:
455
+ if t is None:
456
+ return dict(_FALLBACK_WITHHELD)
457
+ return dict(getattr(t, "withheld", {}) or {})
458
+
459
+
460
+ def note_text(counts: Dict[str, int]) -> str:
461
+ """One plain line: what was left out, and that V2 includes it."""
462
+ total = sum(counts.values())
463
+ if total <= 0:
464
+ return ""
465
+ kinds = sorted({SOURCE_LABELS.get(s, s.replace("_", " ")) for s in counts})
466
+ what = ", ".join(kinds)
467
+ items = "1 item" if total == 1 else f"{total} items"
468
+ return (f"Left out: {items} ({what}) about a client whose agreement keeps their data on "
469
+ f"Hyper's own models, because the model answering now is from an outside "
470
+ f"provider. Switch to HyperSpeed V2 (/model) to include them.")
471
+
472
+
473
+ def reply_note(t: Any) -> str:
474
+ counts = withheld_counts(t)
475
+ if sum(counts.values()) > 0 and profile_missing():
476
+ return PROFILE_MISSING_NOTE
477
+ return note_text(counts)
478
+
479
+
480
+ def message_note(n: int, source: str) -> str:
481
+ if n <= 0:
482
+ return ""
483
+ if profile_missing():
484
+ return (PROFILE_MISSING_NOTE + " Tell the user exactly this; do not guess what was "
485
+ "left out.")
486
+ label = SOURCE_LABELS.get(source, source)
487
+ items = "1 item" if n == 1 else f"{n} items"
488
+ return (f"Withheld {items} from {label}: about a client whose agreement keeps their data on "
489
+ f"Hyper's own models, and the model answering now is from an outside provider. "
490
+ f"Tell the user plainly that {items} {'was' if n == 1 else 'were'} left out and that "
491
+ f"switching to HyperSpeed V2 includes {'it' if n == 1 else 'them'}. Do not guess what "
492
+ f"{'it says' if n == 1 else 'they say'}.")
493
+
494
+
495
+ # ───────────────────────── layer 1: the connector-result boundary ─────────────────────────
496
+
497
+ def involves(value: Any) -> bool:
498
+ """Withhold this? The profile's required list is missing (fail safe),
499
+ it names a restricted client, or it can't be checked. With no list in
500
+ force (and none required) nothing is withheld."""
501
+ if profile_missing():
502
+ return True
503
+ m = matcher()
504
+ if not m._pats:
505
+ return False
506
+ return (not checkable(value)) or m.hits(value)
507
+
508
+
509
+ def filter_items(items: List[Any], source: str, *, outside: Optional[bool] = None,
510
+ record: bool = True) -> Tuple[List[Any], int]:
511
+ """Keep the items that don't involve a restricted client. Under Hyper's
512
+ own model everything is kept. Returns (kept, withheld count)."""
513
+ if outside is None:
514
+ outside = outside_model_active()
515
+ if not outside or not active():
516
+ return list(items), 0
517
+ kept, n = [], 0
518
+ for it in items:
519
+ if involves(it):
520
+ n += 1
521
+ else:
522
+ kept.append(it)
523
+ if record:
524
+ _record(source, n)
525
+ return kept, n
526
+
527
+
528
+ def screen(value: Any, source: str, *, outside: Optional[bool] = None,
529
+ record: bool = True) -> Tuple[Any, int]:
530
+ """Whole-value screen: (value, 0) or (PLACEHOLDER, 1)."""
531
+ if outside is None:
532
+ outside = outside_model_active()
533
+ if not outside or not involves(value):
534
+ return value, 0
535
+ if record:
536
+ _record(source, 1)
537
+ return placeholder(), 1
538
+
539
+
540
+ # Envelope data keys that hold one connector item per list element.
541
+ ITEM_KEYS = ("items", "messages", "threads", "events", "files", "records", "entries",
542
+ "results")
543
+
544
+
545
+ def filter_tool_result(tool: str, env: Any, *, outside: Optional[bool] = None) -> Any:
546
+ """The registry boundary for a connector tool's OK envelope: items that
547
+ involve a restricted client are withheld (lists of items one by one,
548
+ anything else whole), and the message says so. Other envelopes pass."""
549
+ source = CONNECTOR_TOOL_SOURCES.get(tool)
550
+ if source is None or getattr(env, "status", None) != "ok":
551
+ return env
552
+ if outside is None:
553
+ outside = outside_model_active()
554
+ if not outside or not active():
555
+ return env
556
+ data = env.data
557
+ withheld = 0
558
+ if tool in HEADER_ONLY_TOOLS:
559
+ new_data, withheld = placeholder(), 1
560
+ _record(source, 1)
561
+ msg = (f"{tool} can't show message or event bodies, so it can't check them for a client "
562
+ f"whose agreement keeps their data on Hyper's own models; nothing it read is "
563
+ f"shown while an outside model is answering. Use the Google Workspace tools "
564
+ f"(they check each thread and event), or switch to HyperSpeed V2.")
565
+ return env.model_copy(update={"data": new_data, "message": msg})
566
+ if tool not in WHOLE_RESULT_TOOLS and isinstance(data, dict) \
567
+ and any(isinstance(data.get(k), list) for k in ITEM_KEYS):
568
+ new_data = dict(data)
569
+ for k in ITEM_KEYS:
570
+ if isinstance(data.get(k), list):
571
+ kept, n = filter_items(data[k], source, outside=True)
572
+ new_data[k] = kept
573
+ withheld += n
574
+ rest = {k: v for k, v in new_data.items() if k not in ITEM_KEYS
575
+ and k not in ("withheld", "note")}
576
+ if involves(rest): # e.g. a thread subject at the top level
577
+ _record(source, 1)
578
+ withheld += 1
579
+ new_data = {"withheld": withheld, "note": placeholder()}
580
+ elif withheld:
581
+ new_data["withheld"] = int(new_data.get("withheld") or 0) + withheld
582
+ else:
583
+ new_data, withheld = screen(data, source, outside=True)
584
+ msg = env.message
585
+ if involves(msg):
586
+ msg = f"{tool} completed."
587
+ if not withheld:
588
+ withheld = 1
589
+ _record(source, 1)
590
+ new_data = placeholder()
591
+ if not withheld:
592
+ return env
593
+ note = message_note(withheld, source)
594
+ return env.model_copy(update={"data": new_data, "message": (msg + " " + note).strip()})
595
+
596
+
597
+ # ───────────────────────── layer 2: the outgoing request body ─────────────────────────
598
+
599
+ class RestrictedContentBlocked(Exception):
600
+ """An outside model request still carried restricted content that could
601
+ not be withheld: the request was NOT sent."""
602
+
603
+ def __init__(self, provider: str = ""):
604
+ self.provider = provider
605
+ super().__init__(self.user_message)
606
+
607
+ @property
608
+ def user_message(self) -> str:
609
+ return ("Not sent: this request includes content about a client whose agreement keeps "
610
+ "their data on Hyper's own models, and the model answering now is from an "
611
+ "outside provider. Nothing was sent to it. Switch to HyperSpeed V2 (/model), or "
612
+ "start a new conversation (/reset) without that content.")
613
+
614
+
615
+ # Request keys that carry content. Everything else (model, max_tokens, ...)
616
+ # is left alone.
617
+ REQUEST_CONTENT_KEYS = ("system", "messages", "input", "instructions", "prompt", "tools")
618
+ # Inside messages: protocol fields that are never content.
619
+ _PROTOCOL_KEYS = frozenset({"role", "type", "id", "tool_use_id", "tool_call_id", "name",
620
+ "model", "cache_control", "media_type", "stop_reason",
621
+ "index", "status"})
622
+
623
+
624
+ def _redact(value: Any, m: Matcher, counter: list, *, paragraphs: bool = False,
625
+ key: str = "", depth: int = 0) -> Any:
626
+ if depth > 16:
627
+ return value
628
+ if isinstance(value, str):
629
+ if key in _PROTOCOL_KEYS or not m.hits(value):
630
+ return value
631
+ if key == "arguments": # an OpenAI tool call's JSON arguments
632
+ counter[0] += 1
633
+ return json.dumps({"withheld": placeholder()})
634
+ if paragraphs: # the system prompt: only the paragraphs that match
635
+ parts = re.split(r"(\n\s*\n)", value)
636
+ out = []
637
+ for p in parts:
638
+ if p.strip() and m.hits(p):
639
+ counter[0] += 1
640
+ out.append(placeholder())
641
+ else:
642
+ out.append(p)
643
+ return "".join(out)
644
+ counter[0] += 1
645
+ return placeholder()
646
+ if isinstance(value, dict):
647
+ return {k: _redact(v, m, counter, paragraphs=paragraphs, key=str(k), depth=depth + 1)
648
+ for k, v in value.items()}
649
+ if isinstance(value, list):
650
+ return [_redact(v, m, counter, paragraphs=paragraphs, key=key, depth=depth + 1)
651
+ for v in value]
652
+ if isinstance(value, tuple):
653
+ return tuple(_redact(v, m, counter, paragraphs=paragraphs, key=key, depth=depth + 1)
654
+ for v in value)
655
+ if hasattr(value, "model_dump"): # SDK message objects in a history
656
+ try:
657
+ return _redact(value.model_dump(), m, counter, paragraphs=paragraphs, key=key,
658
+ depth=depth + 1) if m.hits(value) else value
659
+ except Exception:
660
+ return value
661
+ return value
662
+
663
+
664
+ class _ConnectorContent:
665
+ """Fail-safe matcher when the profile's required list is missing: any
666
+ string that is connector content (type tag or the taint ledger)."""
667
+
668
+ def hits(self, value: Any) -> bool:
669
+ from hyperclaw import taint
670
+ for s in strings(value):
671
+ if isinstance(s, taint.TaintedText):
672
+ return True
673
+ try:
674
+ if taint._matches_any(s):
675
+ return True
676
+ except Exception:
677
+ return True # can't check: withhold
678
+ return False
679
+
680
+
681
+ def _request_matcher():
682
+ return _ConnectorContent() if profile_missing() else matcher()
683
+
684
+
685
+ def redact_request(provider: str, kw: dict) -> dict:
686
+ """For an SDK call's keyword arguments: under an outside provider, every
687
+ content string that names a restricted client is replaced by the note
688
+ (whole message content; for the system prompt, the matching paragraphs).
689
+ Returns the kwargs to send (a copy when anything changed)."""
690
+ if is_hyper_provider(provider) or not isinstance(kw, dict):
691
+ return kw
692
+ m = _request_matcher()
693
+ counter = [0]
694
+ out = dict(kw)
695
+ for k in REQUEST_CONTENT_KEYS:
696
+ if k in out and out[k] is not None and k != "tools":
697
+ out[k] = _redact(out[k], m, counter, paragraphs=(k in ("system", "instructions")))
698
+ if counter[0]:
699
+ _record("request", counter[0])
700
+ log.info("client guard: withheld %d restricted part(s) from a %s request",
701
+ counter[0], provider or "?")
702
+ return out
703
+ return kw
704
+
705
+
706
+ def final_check(provider: str, request: Any) -> None:
707
+ """Last check before a request leaves: an outside provider's request that
708
+ still involves a restricted client is stopped. Raises
709
+ RestrictedContentBlocked; nothing is sent."""
710
+ if is_hyper_provider(provider):
711
+ return
712
+ if request is None:
713
+ return
714
+ if _request_matcher().hits(request):
715
+ log.warning("client guard: stopped a %s request carrying restricted content",
716
+ provider or "?")
717
+ raise RestrictedContentBlocked(provider)