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,313 @@
1
+ """Where onboarding keeps secrets: the OS secret store if there is one, else
2
+ an owner-only file.
3
+
4
+ Backends (first that applies; ``HYPERCLAW_SECRET_STORE=keychain|secret-tool|file``
5
+ overrides):
6
+
7
+ - ``keychain`` (macOS): generic passwords, service ``hyperclaw``, account =
8
+ the secret's name. Written through ``security -i`` on stdin, so a value is
9
+ never on a command line or in the process list.
10
+ - ``secret-tool`` (Linux with the Secret Service): attributes
11
+ ``service hyperclaw account <name>``; the value goes on stdin.
12
+ - ``file``: ``$HYPERCLAW_ROOT/secrets/team-secrets.json``, directory 0700,
13
+ file 0600. A file readable by group or others is refused.
14
+
15
+ Values are stored base64url-encoded, so any value (a JSON token bundle, for
16
+ instance) is safe on the ``security -i`` command line.
17
+
18
+ Some secrets are read by the rest of HyperAgents from the environment
19
+ (``ANTHROPIC_API_KEY``, ``TELEGRAM_BOT_TOKEN``). The non-secret map
20
+ ``$HYPERCLAW_ROOT/config/secret-env.json`` names them; ``export_env()``
21
+ copies them into the process environment at start-up (never overriding a
22
+ value already set). Every entry point that loads ``.env`` calls it.
23
+
24
+ Nothing here prints, logs or returns a value in an error message.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import base64
29
+ import json
30
+ import logging
31
+ import os
32
+ import re
33
+ import shutil
34
+ import stat
35
+ import subprocess
36
+ import sys
37
+ from pathlib import Path
38
+ from typing import Callable, Optional
39
+
40
+ log = logging.getLogger("hyperclaw.teamsetup.secrets")
41
+
42
+ SERVICE = "hyperclaw"
43
+ STORE_ENV = "HYPERCLAW_SECRET_STORE"
44
+ _NAME = re.compile(r"^[a-z0-9_.-]{1,64}$")
45
+
46
+
47
+ class SecretStoreError(Exception):
48
+ """A secret couldn't be stored or read. The text never holds a value."""
49
+
50
+
51
+ def _root() -> Path:
52
+ from hyperclaw.userconfig import root
53
+ return root()
54
+
55
+
56
+ def _enc(value: str) -> str:
57
+ return base64.urlsafe_b64encode(value.encode("utf-8")).decode("ascii")
58
+
59
+
60
+ def _dec(raw: str) -> str:
61
+ raw = (raw or "").strip()
62
+ if not raw:
63
+ return ""
64
+ try:
65
+ return base64.urlsafe_b64decode(raw.encode("ascii")).decode("utf-8")
66
+ except Exception:
67
+ return ""
68
+
69
+
70
+ def _check_name(name: str) -> str:
71
+ if not _NAME.match(name or ""):
72
+ raise SecretStoreError("invalid secret name")
73
+ return name
74
+
75
+
76
+ # ── file backend ──────────────────────────────────────────────────────────
77
+
78
+ class FileStore:
79
+ name = "file"
80
+ where = "an owner-only file (0600)"
81
+
82
+ def __init__(self, path: Optional[Path] = None):
83
+ self.path = path or (_root() / "secrets" / "team-secrets.json")
84
+
85
+ def _read(self) -> dict:
86
+ p = self.path
87
+ if not p.exists():
88
+ return {}
89
+ if stat.S_IMODE(p.stat().st_mode) & 0o077:
90
+ raise SecretStoreError("the secrets file is readable by other accounts "
91
+ "(it must be 0600)")
92
+ try:
93
+ data = json.loads(p.read_text(encoding="utf-8"))
94
+ except Exception:
95
+ raise SecretStoreError("the secrets file is not valid JSON")
96
+ return data if isinstance(data, dict) else {}
97
+
98
+ def _write(self, data: dict) -> None:
99
+ d = self.path.parent
100
+ d.mkdir(parents=True, exist_ok=True)
101
+ try:
102
+ os.chmod(d, 0o700)
103
+ except OSError:
104
+ pass
105
+ tmp = self.path.with_suffix(".tmp")
106
+ fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
107
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
108
+ json.dump(data, f)
109
+ os.chmod(tmp, 0o600)
110
+ os.replace(tmp, self.path)
111
+
112
+ def get(self, name: str) -> str:
113
+ return _dec(str(self._read().get(_check_name(name)) or ""))
114
+
115
+ def set(self, name: str, value: str) -> None:
116
+ data = self._read()
117
+ data[_check_name(name)] = _enc(value)
118
+ self._write(data)
119
+
120
+ def delete(self, name: str) -> bool:
121
+ data = self._read()
122
+ had = data.pop(_check_name(name), None) is not None
123
+ if had:
124
+ self._write(data)
125
+ return had
126
+
127
+
128
+ # ── macOS keychain ────────────────────────────────────────────────────────
129
+
130
+ Runner = Callable[..., subprocess.CompletedProcess]
131
+
132
+
133
+ class KeychainStore:
134
+ name = "keychain"
135
+ where = "the macOS keychain"
136
+
137
+ def __init__(self, runner: Optional[Runner] = None):
138
+ self.run = runner or subprocess.run
139
+
140
+ def get(self, name: str) -> str:
141
+ r = self.run(["security", "find-generic-password", "-s", SERVICE, "-a",
142
+ _check_name(name), "-w"], capture_output=True, text=True)
143
+ if r.returncode != 0:
144
+ return ""
145
+ return _dec(r.stdout or "")
146
+
147
+ def set(self, name: str, value: str) -> None:
148
+ cmd = f"add-generic-password -U -s {SERVICE} -a {_check_name(name)} -w {_enc(value)}\n"
149
+ r = self.run(["security", "-i"], input=cmd, capture_output=True, text=True)
150
+ if r.returncode != 0:
151
+ raise SecretStoreError(f"the keychain refused the item (exit {r.returncode})")
152
+
153
+ def delete(self, name: str) -> bool:
154
+ r = self.run(["security", "delete-generic-password", "-s", SERVICE, "-a",
155
+ _check_name(name)], capture_output=True, text=True)
156
+ return r.returncode == 0
157
+
158
+
159
+ # ── Linux Secret Service ──────────────────────────────────────────────────
160
+
161
+ class SecretToolStore:
162
+ name = "secret-tool"
163
+ where = "the system keyring"
164
+
165
+ def __init__(self, runner: Optional[Runner] = None):
166
+ self.run = runner or subprocess.run
167
+
168
+ def get(self, name: str) -> str:
169
+ r = self.run(["secret-tool", "lookup", "service", SERVICE, "account", _check_name(name)],
170
+ capture_output=True, text=True)
171
+ return _dec(r.stdout or "") if r.returncode == 0 else ""
172
+
173
+ def set(self, name: str, value: str) -> None:
174
+ r = self.run(["secret-tool", "store", f"--label=HyperAgents {name}", "service", SERVICE,
175
+ "account", _check_name(name)], input=_enc(value), capture_output=True,
176
+ text=True)
177
+ if r.returncode != 0:
178
+ raise SecretStoreError(f"the keyring refused the item (exit {r.returncode})")
179
+
180
+ def delete(self, name: str) -> bool:
181
+ r = self.run(["secret-tool", "clear", "service", SERVICE, "account", _check_name(name)],
182
+ capture_output=True, text=True)
183
+ return r.returncode == 0
184
+
185
+
186
+ def backend_name() -> str:
187
+ env = os.environ.get(STORE_ENV, "").strip().lower()
188
+ if env in ("keychain", "secret-tool", "file"):
189
+ return env
190
+ if sys.platform == "darwin" and shutil.which("security"):
191
+ return "keychain"
192
+ if sys.platform.startswith("linux") and shutil.which("secret-tool") \
193
+ and os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
194
+ return "secret-tool"
195
+ return "file"
196
+
197
+
198
+ def store(name: str = ""):
199
+ n = name or backend_name()
200
+ if n == "keychain":
201
+ return KeychainStore()
202
+ if n == "secret-tool":
203
+ return SecretToolStore()
204
+ return FileStore()
205
+
206
+
207
+ # ── the env map (non-secret) ──────────────────────────────────────────────
208
+
209
+ def env_map_path() -> Path:
210
+ return _root() / "config" / "secret-env.json"
211
+
212
+
213
+ def env_map() -> dict:
214
+ """{"store": backend, "env": {ENV_NAME: secret name}}."""
215
+ try:
216
+ data = json.loads(env_map_path().read_text(encoding="utf-8"))
217
+ if isinstance(data, dict) and isinstance(data.get("env"), dict):
218
+ return data
219
+ except Exception:
220
+ pass
221
+ return {"store": "", "env": {}}
222
+
223
+
224
+ def _write_env_map(data: dict) -> None:
225
+ p = env_map_path()
226
+ p.parent.mkdir(parents=True, exist_ok=True)
227
+ fd = os.open(str(p), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
228
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
229
+ json.dump(data, f, indent=1, sort_keys=True)
230
+
231
+
232
+ def put(name: str, value: str, *, env: str = "", backend=None) -> str:
233
+ """Store a secret; with ``env``, also export it to that variable now and
234
+ at every start. Returns where it was stored (for the ✓ line)."""
235
+ st = backend or store(env_map().get("store") or "")
236
+ st.set(name, value)
237
+ if env:
238
+ m = env_map()
239
+ m["store"] = st.name
240
+ m["env"][env] = name
241
+ _write_env_map(m)
242
+ os.environ[env] = value
243
+ return st.where
244
+
245
+
246
+ def link_env(name: str, env: str, backend=None) -> bool:
247
+ """Export an already stored secret to ``env`` (now and at every start).
248
+ False when the secret isn't stored."""
249
+ st = backend or store(env_map().get("store") or "")
250
+ value = get(name, backend=st)
251
+ if not value:
252
+ return False
253
+ m = env_map()
254
+ m["store"] = st.name
255
+ m["env"][env] = name
256
+ _write_env_map(m)
257
+ os.environ[env] = value
258
+ return True
259
+
260
+
261
+ def unlink_env(env: str) -> None:
262
+ """Stop exporting ``env`` (the secret itself stays stored)."""
263
+ m = env_map()
264
+ if m["env"].pop(env, None) is not None:
265
+ _write_env_map(m)
266
+ os.environ.pop(env, None)
267
+
268
+
269
+ def get(name: str, backend=None) -> str:
270
+ st = backend or store(env_map().get("store") or "")
271
+ try:
272
+ return st.get(name)
273
+ except SecretStoreError:
274
+ return ""
275
+
276
+
277
+ def remove(name: str, backend=None) -> bool:
278
+ st = backend or store(env_map().get("store") or "")
279
+ m = env_map()
280
+ gone = [k for k, v in m["env"].items() if v == name]
281
+ for k in gone:
282
+ m["env"].pop(k, None)
283
+ if gone:
284
+ _write_env_map(m)
285
+ try:
286
+ return st.delete(name)
287
+ except SecretStoreError:
288
+ return False
289
+
290
+
291
+ def export_env(backend=None) -> list:
292
+ """Copy the mapped secrets into os.environ (setdefault). Returns the
293
+ variable names exported. Never raises; a missing map costs nothing."""
294
+ try:
295
+ m = env_map()
296
+ if not m["env"]:
297
+ return []
298
+ st = backend or store(m.get("store") or "")
299
+ done = []
300
+ for env_name, secret_name in m["env"].items():
301
+ if os.environ.get(env_name):
302
+ continue
303
+ try:
304
+ v = st.get(secret_name)
305
+ except SecretStoreError:
306
+ v = ""
307
+ if v:
308
+ os.environ[env_name] = v
309
+ done.append(env_name)
310
+ return done
311
+ except Exception as e: # noqa: BLE001 -- start-up must never fail here
312
+ log.warning("secret store: couldn't export secrets (%s)", type(e).__name__)
313
+ return []
@@ -0,0 +1,295 @@
1
+ """``hyperclaw self-check``: a report a team member can send Vaughn.
2
+
3
+ It writes ``$HYPERCLAW_ROOT/reports/self-check-<UTC time>.txt`` (0600) with:
4
+ version, OS, install method and mode, the onboarding steps, one live check
5
+ per piece (the same "✓ Connected" lines as onboarding), the background
6
+ service state, which secrets are stored (names only), and the recent
7
+ warnings and errors from the logs.
8
+
9
+ Removed from the whole report, whatever their source:
10
+
11
+ - keys and tokens: every value held in the secret store, the digest token
12
+ store, the Google token file and the secret-looking ``.env`` entries is
13
+ replaced literally; known shapes (Anthropic/OpenAI/xAI keys, Telegram bot
14
+ tokens, Slack, GitLab, Google OAuth, GitHub, AWS, JWT, bearer, password=)
15
+ are replaced by pattern as a second line of defence;
16
+ - email addresses;
17
+ - message content: logs keep warnings and errors only; text after message
18
+ markers (``User:``, ``text=``, ...) and anything in quotes is dropped;
19
+ conversation files are never read;
20
+ - file paths with the user name (home is shown as ``~``, the account name as
21
+ ``<user>``) and long numbers such as chat ids.
22
+
23
+ ``tests/unit/teamsetup/test_selfcheck.py`` plants a fake secret of every kind and
24
+ proves none reaches the report.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import datetime as _dt
29
+ import getpass
30
+ import json
31
+ import os
32
+ import platform
33
+ import re
34
+ import sys
35
+ from pathlib import Path
36
+ from typing import Callable, Iterable, Optional
37
+
38
+ LOG_FILES = ("logs/service.log", "logs/telegram.err.log", "logs/hyperclaw.log",
39
+ "logs/telegram.log")
40
+ LOG_TAIL = 400 # lines read from the end of each log
41
+ LOG_KEEP = 60 # warning/error lines kept per log
42
+ _LEVEL = re.compile(r"\b(WARNING|ERROR|CRITICAL)\b")
43
+
44
+ _PATTERNS = [
45
+ (re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)",
46
+ re.S), "[key removed]"),
47
+ (re.compile(r"\bsk-ant-[A-Za-z0-9_-]{8,}"), "[key removed]"),
48
+ (re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}"), "[key removed]"),
49
+ (re.compile(r"\bxai-[A-Za-z0-9_-]{16,}"), "[key removed]"),
50
+ (re.compile(r"\brpa_[A-Za-z0-9_-]{8,}"), "[key removed]"),
51
+ (re.compile(r"\bHCT\d+\.[A-Za-z0-9_.-]{16,}"), "[item removed]"),
52
+ (re.compile(r"\b\d{5,}:[A-Za-z0-9_-]{30,}"), "[token removed]"),
53
+ (re.compile(r"\bbot\d{5,}:[A-Za-z0-9_-]{20,}"), "[token removed]"),
54
+ (re.compile(r"\bxox[abeoprsc]-[A-Za-z0-9-]{8,}"), "[token removed]"),
55
+ (re.compile(r"\bxoxe[.-][A-Za-z0-9.-]{8,}"), "[token removed]"),
56
+ (re.compile(r"\bxapp-[A-Za-z0-9-]{8,}"), "[token removed]"),
57
+ (re.compile(r"\bgl[a-z]{1,8}-[A-Za-z0-9_.-]{16,}"), "[token removed]"),
58
+ (re.compile(r"\bya29\.[A-Za-z0-9_.-]{8,}"), "[token removed]"),
59
+ (re.compile(r"\b1//[A-Za-z0-9_-]{8,}"), "[token removed]"),
60
+ (re.compile(r"\bGOCSPX-[A-Za-z0-9_-]{8,}"), "[secret removed]"),
61
+ (re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}"), "[token removed]"),
62
+ (re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b"), "[key removed]"),
63
+ (re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}"),
64
+ "[token removed]"),
65
+ (re.compile(r"(?i)\b(bearer\s+)[A-Za-z0-9._~+/=-]{8,}"), r"\1[token removed]"),
66
+ (re.compile(r"(?i)\b((?:password|passwd|secret|api[_-]?key|access[_-]?token|"
67
+ r"refresh[_-]?token|token|key|code|code_verifier)\s*[:=]\s*)"
68
+ r"(?!\[)(\"[^\"]*\"|'[^']*'|\S+)"), r"\1[removed]"),
69
+ (re.compile(r"\b([a-z][a-z0-9+.-]*://[^:/\s@]+:)[^@\s]+(@)"), r"\1[removed]\2"),
70
+ (re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "[email removed]"),
71
+ # any account's home folder, not only the current one
72
+ (re.compile(r"(/(?:Users|home))/(?!Shared/)[^/\s]+"), r"\1/<user>"),
73
+ (re.compile(r"(?<![\w.])-?\d{6,}(?![\w.])"), "[number removed]"),
74
+ ]
75
+ # Text after these markers is message content.
76
+ _CONTENT = re.compile(r"(?i)\b(user|assistant|reply|message|text|prompt|content|said|"
77
+ r"caption|body|query)\s*[:=]\s*.*$")
78
+ _QUOTED = re.compile(r"\"[^\"]*\"|'[^']*'|“[^”]*”|‘[^’]*’")
79
+ _SECRET_ENV = re.compile(r"(KEY|TOKEN|SECRET|PASSWORD|PASS|DATABASE_URL|DSN|CREDENTIAL)",
80
+ re.I)
81
+
82
+
83
+ def known_secret_values() -> list:
84
+ """Every secret value this install holds (to remove literally)."""
85
+ vals = set()
86
+ from hyperclaw.teamsetup import secrets
87
+ try:
88
+ for name in ("model_key", "hyper_key", "google_client_secret", "telegram_bot_token",
89
+ "slack_user_token"):
90
+ v = secrets.get(name)
91
+ if v:
92
+ vals.add(v)
93
+ if name == "slack_user_token":
94
+ try:
95
+ b = json.loads(v)
96
+ vals.update(str(x) for k, x in b.items()
97
+ if k in ("access_token", "refresh_token") and x)
98
+ except ValueError:
99
+ pass
100
+ except Exception:
101
+ pass
102
+ try:
103
+ from hyperclaw.digest import tokens
104
+ for src in ("gitlab", "slack"):
105
+ for backend in {tokens.backend_name(), "file"}:
106
+ try:
107
+ v = tokens.store(backend).get(src)
108
+ except Exception:
109
+ v = ""
110
+ if v:
111
+ vals.add(v)
112
+ except Exception:
113
+ pass
114
+ try:
115
+ from hyperclaw.google_workspace import auth as ga
116
+ tok = ga.load_token()
117
+ vals.update(str(tok.get(k)) for k in ("refresh_token", "access_token") if tok.get(k))
118
+ cp = ga.client_path()
119
+ if cp.is_file():
120
+ vals.add(str((json.loads(cp.read_text()).get("installed") or {})
121
+ .get("client_secret") or ""))
122
+ except Exception:
123
+ pass
124
+ try:
125
+ from hyperclaw.brief import store as bstore
126
+ tok = bstore.read_json(bstore.token_path())
127
+ vals.update(str(tok.get(k)) for k in ("refresh_token", "access_token") if tok.get(k))
128
+ except Exception:
129
+ pass
130
+ try:
131
+ from hyperclaw.userconfig import env_read
132
+ for k, v in env_read().items():
133
+ if v and _SECRET_ENV.search(k):
134
+ vals.add(v)
135
+ except Exception:
136
+ pass
137
+ for k, v in os.environ.items():
138
+ if v and _SECRET_ENV.search(k) and len(v) >= 8:
139
+ vals.add(v)
140
+ return sorted((v for v in vals if len(v) >= 6), key=len, reverse=True)
141
+
142
+
143
+ class Redactor:
144
+ def __init__(self, values: Optional[Iterable[str]] = None, home: str = "",
145
+ user: str = ""):
146
+ self.values = list(values if values is not None else known_secret_values())
147
+ self.home = home or str(Path.home())
148
+ self.user = user or _user()
149
+
150
+ def __call__(self, text: str) -> str:
151
+ out = str(text or "")
152
+ for v in self.values:
153
+ out = out.replace(v, "[secret removed]")
154
+ if self.home and self.home not in ("/", ""):
155
+ out = out.replace(self.home, "~")
156
+ for pat, repl in _PATTERNS:
157
+ out = pat.sub(repl, out)
158
+ if self.user and len(self.user) >= 3:
159
+ out = re.sub(rf"(?<![A-Za-z0-9]){re.escape(self.user)}(?![A-Za-z0-9])", "<user>",
160
+ out)
161
+ return out
162
+
163
+ def log_line(self, line: str) -> str:
164
+ """A log line with message content removed, then redacted."""
165
+ s = _CONTENT.sub(lambda m: m.group(1) + ": [message removed]", line)
166
+ s = _QUOTED.sub("[text removed]", s)
167
+ return self(s)[:300]
168
+
169
+
170
+ def _user() -> str:
171
+ try:
172
+ return getpass.getuser()
173
+ except Exception:
174
+ return os.environ.get("USER", "")
175
+
176
+
177
+ def log_section(redact: Redactor) -> list:
178
+ from hyperclaw.userconfig import root
179
+ out = []
180
+ for rel in LOG_FILES:
181
+ p = root() / rel
182
+ if not p.exists():
183
+ continue
184
+ try:
185
+ with open(p, "rb") as f:
186
+ f.seek(0, 2)
187
+ size = f.tell()
188
+ f.seek(max(0, size - 256 * 1024))
189
+ lines = f.read().decode("utf-8", errors="replace").splitlines()[-LOG_TAIL:]
190
+ except OSError:
191
+ continue
192
+ kept = [redact.log_line(x) for x in lines if _LEVEL.search(x)][-LOG_KEEP:]
193
+ out.append(f"--- {rel}: {len(lines)} recent lines, {len(kept)} warnings/errors shown "
194
+ "(message text removed)")
195
+ out += kept
196
+ return out or ["(no logs yet)"]
197
+
198
+
199
+ def _secret_names() -> list:
200
+ from hyperclaw.teamsetup import secrets
201
+ out = []
202
+ st = secrets.env_map().get("store") or secrets.backend_name()
203
+ for name in ("model_key", "hyper_key", "google_client_secret", "telegram_bot_token",
204
+ "slack_user_token"):
205
+ try:
206
+ present = bool(secrets.get(name))
207
+ except Exception:
208
+ present = False
209
+ out.append(f"{name}: {'stored' if present else 'not stored'}")
210
+ out.append(f"secret store: {st}")
211
+ return out
212
+
213
+
214
+ def build(*, checks_fn: Optional[Callable[[], list]] = None,
215
+ service_fn: Optional[Callable[[], dict]] = None,
216
+ redact: Optional[Redactor] = None, now: Optional[_dt.datetime] = None) -> str:
217
+ from hyperclaw import __version__, distribution
218
+ from hyperclaw.deployment import mode
219
+ from hyperclaw.teamsetup import onboard, profile, service
220
+ redact = redact or Redactor()
221
+ now = now or _dt.datetime.now(_dt.timezone.utc)
222
+ try:
223
+ from hyperclaw.updates.install import detect
224
+ method = detect().kind
225
+ except Exception:
226
+ method = "other"
227
+ prof = profile.load()
228
+ lines = [
229
+ "HyperAgents self-check",
230
+ f"time (UTC): {now.strftime('%Y-%m-%d %H:%M')}",
231
+ f"version: {__version__}",
232
+ f"python: {platform.python_version()}",
233
+ f"system: {platform.system()} {platform.mac_ver()[0] or platform.release()} "
234
+ f"({platform.machine()})",
235
+ f"install: {method}; mode: {mode()}; team profile: "
236
+ f"{prof.get('name', 'none') if prof else 'none or not verified'}",
237
+ f"install command: {distribution.install_command(__version__)}"
238
+ + (f" (this install is the old package; {distribution.migration_line(__version__)})"
239
+ if distribution.is_legacy_install() else ""),
240
+ "",
241
+ "Onboarding steps:",
242
+ ]
243
+ st = onboard.load_state()["steps"]
244
+ for s in onboard.STEPS:
245
+ info = st.get(s) or {}
246
+ lines.append(f" {s}: {info.get('status') or 'not started'}"
247
+ + (f" ({info['code']})" if info.get("code") else ""))
248
+ lines += ["", "Connections:"]
249
+ try:
250
+ conn = checks_fn() if checks_fn else onboard.summary_lines()
251
+ except Exception as e: # noqa: BLE001
252
+ conn = [f"(checks couldn't run: {type(e).__name__})"]
253
+ lines += [f" {x}" for x in conn]
254
+ try:
255
+ svc = service_fn() if service_fn else service.status()
256
+ except Exception as e: # noqa: BLE001
257
+ svc = {"error": type(e).__name__}
258
+ lines += ["", "Background service: " + ", ".join(f"{k}={v}" for k, v in svc.items()),
259
+ "scheduler jobs: " + (os.environ.get("HYPERCLAW_SCHEDULER_JOBS", "")
260
+ or "(set in the service only)"),
261
+ "", "Stored secrets (names only):"]
262
+ lines += [f" {x}" for x in _secret_names()]
263
+ lines += ["", "Recent warnings and errors:"]
264
+ lines += log_section(redact)
265
+ lines += ["", "Removed from this report: keys, tokens, email addresses, message text, "
266
+ "and your user name in file paths."]
267
+ return redact("\n".join(lines)) + "\n"
268
+
269
+
270
+ def write(text: str, out: Optional[Path] = None) -> Path:
271
+ from hyperclaw.userconfig import root
272
+ if out is None:
273
+ stamp = _dt.datetime.now(_dt.timezone.utc).strftime("%Y%m%d-%H%M%S")
274
+ out = root() / "reports" / f"self-check-{stamp}.txt"
275
+ out.parent.mkdir(parents=True, exist_ok=True)
276
+ fd = os.open(str(out), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
277
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
278
+ f.write(text)
279
+ return out
280
+
281
+
282
+ def main(out: Optional[str] = None, *, echo: Callable[[str], None] = print) -> Path:
283
+ from hyperclaw.userconfig import load_env_into_process
284
+ load_env_into_process()
285
+ text = build()
286
+ p = write(text, Path(out).expanduser() if out else None)
287
+ shown = str(p).replace(str(Path.home()), "~")
288
+ echo(f"Self-check written to {shown}")
289
+ echo("It has no keys, tokens, email addresses or messages in it. Send that file to Vaughn "
290
+ "(attach it in a direct message).")
291
+ return p
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main(sys.argv[1] if len(sys.argv) > 1 else None)